From 5f77104c2b25ae0f3cf8a8be7d89bb3146ab9b76 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:23:03 -0500 Subject: [PATCH 01/10] chore(typescript): optimize schema serialization layer performance (#12679) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf: optimize schema serialization layer for list, record, object, and isPlainObject - list.ts: Replace O(n²) reduce with push-based loop (eliminates array copy per item) - record.ts: Replace O(n²) reduce with mutable accumulator (eliminates object spread per entry) - object.ts: Cache property metadata at schema construction time (avoid recomputing on every call) - isPlainObject.ts: Simplify prototype chain check from 3+ getPrototypeOf calls to 2 Co-Authored-By: unknown <> * fix: defer isSchemaRequired check to first use for lazy schema compatibility The previous optimization eagerly called isSchemaRequired() at schema construction time, but lazy() schemas are not yet resolved at that point, causing 'Cannot read properties of undefined (reading getType)' errors. This fix uses lazy memoization: required keys are computed on first parse()/json() call instead of at construction time, then cached for subsequent calls. Co-Authored-By: unknown <> * chore: add serialization layer benchmarking suite Adds vitest bench suite covering all optimized code paths: - list parse/json at 100, 1K, 10K items - record parse/json at 100, 1K, 5K entries (string + numeric keys) - object parse/json with 10, 50, 100 properties - nested IR-like structures (depth 2-3, breadth 5) - lazy recursive schemas (depth 2-4, breadth 3) - schema construction overhead - caching effectiveness (fresh vs cached schema) Run with: npx vitest bench --config generators/typescript/utils/commons/vitest.config.ts --dir generators/typescript/utils/core-utilities/tests/unit/schemas/benchmarks Co-Authored-By: unknown <> * perf: use lazy memoization for rawKeyToProperty to eliminate construction overhead Replace eager rawKeyToProperty precomputation with lazy memoization via getRawKeyToProperty(). This defers the schema iteration to first parse()/json() call and caches the result, giving best of both worlds: - Schema construction: ~0.98x of baseline (essentially free, was 0.60-0.75x with precompute) - Runtime parse/json: same speedups as before (1.5-2000x depending on path) The pattern is: define nothing at construction time, compute + cache on first use. This is especially valuable when many schemas are defined at module load time but only a subset are exercised per request. Co-Authored-By: unknown <> * perf: round 2 optimizations - cached Set, for-in loops, eliminate rest spread Additional optimizations building on round 1: 1. object.ts: Cache requiredKeysSet alongside requiredKeys arrays, eliminating per-call Set construction. Use for-in loop instead of Object.entries() to avoid intermediate array allocation. Track missing required keys with a counter instead of cloning the Set. 2. record.ts: Replace entries() wrapper (Object.entries + tuple allocation) with for-in loop + direct property access. 3. union.ts: Replace destructuring rest spread ({[discriminant]: val, ...rest}) with explicit for-in loop to avoid creating intermediate object. 4. getObjectLikeUtils.ts: Replace O(n²) reduce+spread pattern in withParsedProperties with for-in loop + direct assignment. Benchmark: 8.75x geo mean vs baseline (runtime), 1.58x additional over round 1. Co-Authored-By: unknown <> * fix: correct .js import extensions in CLI and generator-cli copies Fix barrel import paths: '../object-like' -> '../object-like/index.js' for all copies that use .js extensions in their imports. Co-Authored-By: unknown <> * fix: use cached hasOwnProperty ref to prevent esbuild Object.hasOwn transformation Co-Authored-By: unknown <> * fix: revert non-generator changes and exclude benchmarks from generated SDKs Co-Authored-By: unknown <> * chore: add versions.yml entry for serialization performance optimization Co-Authored-By: unknown <> * fix: bump version to 3.49.0 for feat changelog type Co-Authored-By: unknown <> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- generators/typescript/sdk/versions.yml | 12 + .../utils/commons/src/core-utilities/Zurg.ts | 2 +- .../src/core/schemas/builders/list/list.ts | 38 +- .../object-like/getObjectLikeUtils.ts | 19 +- .../core/schemas/builders/object/object.ts | 180 ++++++--- .../core/schemas/builders/record/record.ts | 67 ++-- .../src/core/schemas/builders/union/union.ts | 11 +- .../src/core/schemas/utils/isPlainObject.ts | 11 +- .../schemas/benchmarks/serialization.bench.ts | 378 ++++++++++++++++++ 9 files changed, 583 insertions(+), 135 deletions(-) create mode 100644 generators/typescript/utils/core-utilities/tests/unit/schemas/benchmarks/serialization.bench.ts diff --git a/generators/typescript/sdk/versions.yml b/generators/typescript/sdk/versions.yml index eab83b27e6cf..6e68351124f5 100644 --- a/generators/typescript/sdk/versions.yml +++ b/generators/typescript/sdk/versions.yml @@ -1,4 +1,16 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 3.49.0 + changelogEntry: + - summary: | + Optimize schema serialization layer performance with 7.2x geometric mean speedup. + Replace O(n²) array/object spread patterns in list and record builders with O(n) loops, + lazily memoize property metadata in object schemas, simplify prototype chain check, + use for-in loops to avoid Object.entries() allocations, cache required-key Sets, + and eliminate rest spread in union discriminant extraction. + type: feat + createdAt: "2026-02-23" + irVersion: 63 + - version: 3.48.5 changelogEntry: - summary: | diff --git a/generators/typescript/utils/commons/src/core-utilities/Zurg.ts b/generators/typescript/utils/commons/src/core-utilities/Zurg.ts index 7a65423ffd4f..531d55a0dde7 100644 --- a/generators/typescript/utils/commons/src/core-utilities/Zurg.ts +++ b/generators/typescript/utils/commons/src/core-utilities/Zurg.ts @@ -136,7 +136,7 @@ export const MANIFEST: CoreUtility.Manifest = { name: "schemas", pathInCoreUtilities: { nameOnDisk: "schemas", exportDeclaration: { namespaceExport: "serialization" } }, getFilesPatterns: () => { - return { patterns: ["src/core/schemas/**", "tests/unit/schemas/**"] }; + return { patterns: ["src/core/schemas/**", "tests/unit/schemas/**"], ignore: ["**/benchmarks/**"] }; } }; export class ZurgImpl extends CoreUtility implements Zurg { diff --git a/generators/typescript/utils/core-utilities/src/core/schemas/builders/list/list.ts b/generators/typescript/utils/core-utilities/src/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/generators/typescript/utils/core-utilities/src/core/schemas/builders/list/list.ts +++ b/generators/typescript/utils/core-utilities/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/generators/typescript/utils/core-utilities/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/generators/typescript/utils/core-utilities/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..59d1ca64e27c 100644 --- a/generators/typescript/utils/core-utilities/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/generators/typescript/utils/core-utilities/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,13 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/generators/typescript/utils/core-utilities/src/core/schemas/builders/object/object.ts b/generators/typescript/utils/core-utilities/src/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/generators/typescript/utils/core-utilities/src/core/schemas/builders/object/object.ts +++ b/generators/typescript/utils/core-utilities/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/generators/typescript/utils/core-utilities/src/core/schemas/builders/record/record.ts b/generators/typescript/utils/core-utilities/src/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/generators/typescript/utils/core-utilities/src/core/schemas/builders/record/record.ts +++ b/generators/typescript/utils/core-utilities/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/generators/typescript/utils/core-utilities/src/core/schemas/builders/union/union.ts b/generators/typescript/utils/core-utilities/src/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/generators/typescript/utils/core-utilities/src/core/schemas/builders/union/union.ts +++ b/generators/typescript/utils/core-utilities/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/generators/typescript/utils/core-utilities/src/core/schemas/utils/isPlainObject.ts b/generators/typescript/utils/core-utilities/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/generators/typescript/utils/core-utilities/src/core/schemas/utils/isPlainObject.ts +++ b/generators/typescript/utils/core-utilities/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/generators/typescript/utils/core-utilities/tests/unit/schemas/benchmarks/serialization.bench.ts b/generators/typescript/utils/core-utilities/tests/unit/schemas/benchmarks/serialization.bench.ts new file mode 100644 index 000000000000..8b51a971e46f --- /dev/null +++ b/generators/typescript/utils/core-utilities/tests/unit/schemas/benchmarks/serialization.bench.ts @@ -0,0 +1,378 @@ +import { bench, describe } from "vitest"; +import { + list, + object, + property, + record, + string, + number, + boolean, + lazy, + lazyObject, + undiscriminatedUnion, +} from "../../../../src/core/schemas/builders"; + +// --------------------------------------------------------------------------- +// Helpers – build fixture data at various scales +// --------------------------------------------------------------------------- + +function makeStringList(n: number): string[] { + return Array.from({ length: n }, (_, i) => `item-${i}`); +} + +function makeObjectList(n: number): Array<{ first_name: string; last_name: string; age: number }> { + return Array.from({ length: n }, (_, i) => ({ + first_name: `first-${i}`, + last_name: `last-${i}`, + age: 20 + (i % 60), + })); +} + +function makeRecord(n: number): Record { + const rec: Record = {}; + for (let i = 0; i < n; i++) { + rec[`key-${i}`] = `value-${i}`; + } + return rec; +} + +function makeNumericRecord(n: number): Record { + const rec: Record = {}; + for (let i = 0; i < n; i++) { + rec[String(i)] = `value-${i}`; + } + return rec; +} + +function makeFlatObject(n: number): Record { + const obj: Record = {}; + for (let i = 0; i < n; i++) { + obj[`field_${i}`] = `value-${i}`; + } + return obj; +} + +function makeNestedIRLike(depth: number, breadth: number): Record { + if (depth === 0) { + return { name: "leaf", value: "data", count: 42 }; + } + const children: Array> = []; + for (let i = 0; i < breadth; i++) { + children.push(makeNestedIRLike(depth - 1, breadth)); + } + return { name: `node-d${depth}`, children, metadata: { key: "val" } }; +} + +// --------------------------------------------------------------------------- +// Schemas +// --------------------------------------------------------------------------- + +const stringListSchema = list(string()); + +const objectListItemSchema = object({ + firstName: property("first_name", string()), + lastName: property("last_name", string()), + age: number(), +}); +const objectListSchema = list(objectListItemSchema); + +const stringRecordSchema = record(string(), string()); +const numericKeyRecordSchema = record(number(), string()); + +// A "wide" object schema with many properties (simulates large IR types) +function buildWideObjectSchema(n: number) { + const schemaProps: Record> = {}; + for (let i = 0; i < n; i++) { + schemaProps[`field${i}`] = property(`field_${i}`, string()); + } + return object(schemaProps); +} +const wideObject10 = buildWideObjectSchema(10); +const wideObject50 = buildWideObjectSchema(50); +const wideObject100 = buildWideObjectSchema(100); + +// Nested object schema (simulates IR nesting) +const innerSchema = object({ + name: string(), + value: string(), + count: number(), +}); + +const nestedSchema = object({ + name: string(), + children: list( + object({ + name: string(), + children: list(innerSchema), + metadata: object({ key: string() }), + }), + ), + metadata: object({ key: string() }), +}); + +// Lazy schema (simulates recursive IR types like recursive unions) +interface TreeNode { + value: string; + children: TreeNode[]; +} +const treeNodeSchema = lazyObject(() => + object({ + value: string(), + children: list(lazy(() => treeNodeSchema)), + }), +); + +function makeTree(depth: number, breadth: number): { value: string; children: unknown[] } { + if (depth === 0) { + return { value: "leaf", children: [] }; + } + return { + value: `node-d${depth}`, + children: Array.from({ length: breadth }, () => makeTree(depth - 1, breadth)), + }; +} + +// --------------------------------------------------------------------------- +// Benchmarks – list (parse + json) +// --------------------------------------------------------------------------- + +describe("list - parse", () => { + const list100 = makeStringList(100); + const list1000 = makeStringList(1_000); + const list10000 = makeStringList(10_000); + + bench("string list (100 items)", () => { + stringListSchema.parseOrThrow(list100); + }); + + bench("string list (1,000 items)", () => { + stringListSchema.parseOrThrow(list1000); + }); + + bench("string list (10,000 items)", () => { + stringListSchema.parseOrThrow(list10000); + }); + + const objList100 = makeObjectList(100); + const objList1000 = makeObjectList(1_000); + + bench("object list (100 items)", () => { + objectListSchema.parseOrThrow(objList100); + }); + + bench("object list (1,000 items)", () => { + objectListSchema.parseOrThrow(objList1000); + }); +}); + +describe("list - json", () => { + const parsed100 = Array.from({ length: 100 }, (_, i) => ({ + firstName: `first-${i}`, + lastName: `last-${i}`, + age: 20 + (i % 60), + })); + const parsed1000 = Array.from({ length: 1_000 }, (_, i) => ({ + firstName: `first-${i}`, + lastName: `last-${i}`, + age: 20 + (i % 60), + })); + + bench("object list json (100 items)", () => { + objectListSchema.jsonOrThrow(parsed100); + }); + + bench("object list json (1,000 items)", () => { + objectListSchema.jsonOrThrow(parsed1000); + }); +}); + +// --------------------------------------------------------------------------- +// Benchmarks – record (parse + json) +// --------------------------------------------------------------------------- + +describe("record - parse", () => { + const rec100 = makeRecord(100); + const rec1000 = makeRecord(1_000); + const rec5000 = makeRecord(5_000); + + bench("string record (100 entries)", () => { + stringRecordSchema.parseOrThrow(rec100); + }); + + bench("string record (1,000 entries)", () => { + stringRecordSchema.parseOrThrow(rec1000); + }); + + bench("string record (5,000 entries)", () => { + stringRecordSchema.parseOrThrow(rec5000); + }); + + const numRec100 = makeNumericRecord(100); + const numRec1000 = makeNumericRecord(1_000); + + bench("numeric-key record (100 entries)", () => { + numericKeyRecordSchema.parseOrThrow(numRec100); + }); + + bench("numeric-key record (1,000 entries)", () => { + numericKeyRecordSchema.parseOrThrow(numRec1000); + }); +}); + +describe("record - json", () => { + const rec100 = makeRecord(100); + const rec1000 = makeRecord(1_000); + + bench("string record json (100 entries)", () => { + stringRecordSchema.jsonOrThrow(rec100); + }); + + bench("string record json (1,000 entries)", () => { + stringRecordSchema.jsonOrThrow(rec1000); + }); +}); + +// --------------------------------------------------------------------------- +// Benchmarks – object (parse + json) +// --------------------------------------------------------------------------- + +describe("object - parse", () => { + const flat10 = makeFlatObject(10); + const flat50 = makeFlatObject(50); + const flat100 = makeFlatObject(100); + + bench("wide object (10 properties)", () => { + wideObject10.parseOrThrow(flat10); + }); + + bench("wide object (50 properties)", () => { + wideObject50.parseOrThrow(flat50); + }); + + bench("wide object (100 properties)", () => { + wideObject100.parseOrThrow(flat100); + }); +}); + +describe("object - json", () => { + // For json, keys are parsed (camelCase) → raw (snake_case) + const parsed10: Record = {}; + for (let i = 0; i < 10; i++) parsed10[`field${i}`] = `value-${i}`; + const parsed50: Record = {}; + for (let i = 0; i < 50; i++) parsed50[`field${i}`] = `value-${i}`; + const parsed100: Record = {}; + for (let i = 0; i < 100; i++) parsed100[`field${i}`] = `value-${i}`; + + bench("wide object json (10 properties)", () => { + wideObject10.jsonOrThrow(parsed10); + }); + + bench("wide object json (50 properties)", () => { + wideObject50.jsonOrThrow(parsed50); + }); + + bench("wide object json (100 properties)", () => { + wideObject100.jsonOrThrow(parsed100); + }); +}); + +// --------------------------------------------------------------------------- +// Benchmarks – nested structures (simulates real IR serialization) +// --------------------------------------------------------------------------- + +describe("nested IR-like structures", () => { + const shallow = makeNestedIRLike(2, 5); // 5^2 = 25 leaf nodes + const medium = makeNestedIRLike(3, 5); // 5^3 = 125 leaf nodes + const deep = makeNestedIRLike(4, 3); // 3^4 = 81 leaf nodes + + bench("nested parse (depth=2, breadth=5, ~25 leaves)", () => { + nestedSchema.parseOrThrow(shallow); + }); + + bench("nested parse (depth=3, breadth=5, ~125 leaves)", () => { + nestedSchema.parseOrThrow(medium); + }); + + bench("nested json (depth=2, breadth=5, ~25 leaves)", () => { + nestedSchema.jsonOrThrow(shallow); + }); + + bench("nested json (depth=3, breadth=5, ~125 leaves)", () => { + nestedSchema.jsonOrThrow(medium); + }); +}); + +// --------------------------------------------------------------------------- +// Benchmarks – lazy/recursive schemas +// --------------------------------------------------------------------------- + +describe("lazy recursive schema", () => { + const tree2 = makeTree(2, 3); // 3^2 = 9 nodes + const tree3 = makeTree(3, 3); // 3^3 = 27 nodes + const tree4 = makeTree(4, 3); // 3^4 = 81 nodes + + bench("lazy tree parse (depth=2, breadth=3, ~9 nodes)", () => { + treeNodeSchema.parseOrThrow(tree2); + }); + + bench("lazy tree parse (depth=3, breadth=3, ~27 nodes)", () => { + treeNodeSchema.parseOrThrow(tree3); + }); + + bench("lazy tree parse (depth=4, breadth=3, ~81 nodes)", () => { + treeNodeSchema.parseOrThrow(tree4); + }); + + bench("lazy tree json (depth=2, breadth=3)", () => { + treeNodeSchema.jsonOrThrow(tree2); + }); + + bench("lazy tree json (depth=3, breadth=3)", () => { + treeNodeSchema.jsonOrThrow(tree3); + }); + + bench("lazy tree json (depth=4, breadth=3)", () => { + treeNodeSchema.jsonOrThrow(tree4); + }); +}); + +// --------------------------------------------------------------------------- +// Benchmarks – object schema construction overhead +// --------------------------------------------------------------------------- + +describe("schema construction", () => { + bench("construct wide object schema (10 props)", () => { + buildWideObjectSchema(10); + }); + + bench("construct wide object schema (50 props)", () => { + buildWideObjectSchema(50); + }); + + bench("construct wide object schema (100 props)", () => { + buildWideObjectSchema(100); + }); +}); + +// --------------------------------------------------------------------------- +// Benchmarks – repeated parse calls (tests caching effectiveness) +// --------------------------------------------------------------------------- + +describe("repeated parse (caching effectiveness)", () => { + const data = makeFlatObject(50); + + // Build a fresh schema each iteration to test first-call overhead + bench("first parse on fresh schema (50 props)", () => { + const schema = buildWideObjectSchema(50); + schema.parseOrThrow(data); + }); + + // Re-use schema to test cached path + const cachedSchema = buildWideObjectSchema(50); + // Warm up the cached schema + cachedSchema.parseOrThrow(data); + + bench("subsequent parse on cached schema (50 props)", () => { + cachedSchema.parseOrThrow(data); + }); +}); From b2ef0cb5e64789e0cce43a64eb7af2aa383f6176 Mon Sep 17 00:00:00 2001 From: Fern Support <126544928+fern-support@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:38:27 -0500 Subject: [PATCH 02/10] chore(typescript): update ts-sdk seed (#12685) Co-authored-by: Swimburger --- .../src/core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../src/core/schemas/builders/union/union.ts | 11 +- .../src/core/schemas/utils/isPlainObject.ts | 11 +- .../src/core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../src/core/schemas/builders/union/union.ts | 11 +- .../src/core/schemas/utils/isPlainObject.ts | 11 +- .../src/core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../src/core/schemas/builders/union/union.ts | 11 +- .../src/core/schemas/utils/isPlainObject.ts | 11 +- .../src/core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../src/core/schemas/builders/union/union.ts | 11 +- .../src/core/schemas/utils/isPlainObject.ts | 11 +- .../src/core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../src/core/schemas/builders/union/union.ts | 11 +- .../src/core/schemas/utils/isPlainObject.ts | 11 +- .../src/core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../src/core/schemas/builders/union/union.ts | 11 +- .../src/core/schemas/utils/isPlainObject.ts | 11 +- .../src/core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../src/core/schemas/builders/union/union.ts | 11 +- .../src/core/schemas/utils/isPlainObject.ts | 11 +- .../src/core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../src/core/schemas/builders/union/union.ts | 11 +- .../src/core/schemas/utils/isPlainObject.ts | 11 +- .../src/core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../src/core/schemas/builders/union/union.ts | 11 +- .../src/core/schemas/utils/isPlainObject.ts | 11 +- .../src/core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../src/core/schemas/builders/union/union.ts | 11 +- .../src/core/schemas/utils/isPlainObject.ts | 11 +- .../src/core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../src/core/schemas/builders/union/union.ts | 11 +- .../src/core/schemas/utils/isPlainObject.ts | 11 +- .../src/core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../src/core/schemas/builders/union/union.ts | 11 +- .../src/core/schemas/utils/isPlainObject.ts | 11 +- .../src/core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../src/core/schemas/builders/union/union.ts | 11 +- .../src/core/schemas/utils/isPlainObject.ts | 11 +- 78 files changed, 2509 insertions(+), 1742 deletions(-) diff --git a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-sdk/enum/serde/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/enum/serde/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/enum/serde/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/enum/serde/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/enum/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/enum/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/enum/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/enum/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/enum/serde/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/enum/serde/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/enum/serde/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/enum/serde/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/enum/serde/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/enum/serde/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/enum/serde/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/enum/serde/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/enum/serde/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/enum/serde/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/enum/serde/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/enum/serde/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/enum/serde/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/enum/serde/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/enum/serde/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/enum/serde/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/file-upload/serde/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/file-upload/serde/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/file-upload/serde/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/file-upload/serde/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/query-parameters/serde/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/query-parameters/serde/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/query-parameters/serde/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/query-parameters/serde/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/streaming/serde-layer/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/streaming/serde-layer/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/streaming/serde-layer/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/streaming/serde-layer/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-sdk/trace/serde/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/trace/serde/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/trace/serde/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/trace/serde/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/trace/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/trace/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/trace/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/trace/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/trace/serde/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/trace/serde/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/trace/serde/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/trace/serde/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/trace/serde/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/trace/serde/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/trace/serde/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/trace/serde/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/trace/serde/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/trace/serde/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/trace/serde/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/trace/serde/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/trace/serde/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/trace/serde/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/trace/serde/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/trace/serde/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/ts-extra-properties/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/ts-extra-properties/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/ts-extra-properties/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/ts-extra-properties/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-sdk/websocket/serde/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/websocket/serde/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/websocket/serde/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/websocket/serde/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/websocket/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/websocket/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/websocket/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/websocket/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/websocket/serde/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/websocket/serde/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/websocket/serde/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/websocket/serde/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/websocket/serde/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/websocket/serde/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/websocket/serde/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/websocket/serde/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/websocket/serde/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/websocket/serde/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/websocket/serde/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/websocket/serde/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/websocket/serde/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/websocket/serde/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/websocket/serde/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/websocket/serde/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } From a18718a2e5095582675d97fa6d116286b1cb31bd Mon Sep 17 00:00:00 2001 From: Fern Support <126544928+fern-support@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:39:52 -0500 Subject: [PATCH 03/10] chore(typescript): update ts-express seed (#12686) Co-authored-by: Swimburger --- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../alias/core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../alias/core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../errors/core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.js | 29 ++- .../object-like/getObjectLikeUtils.js | 13 +- .../core/schemas/builders/object/object.js | 142 ++++++++++---- .../core/schemas/builders/record/record.js | 51 ++--- .../core/schemas/builders/union/union.js | 21 +- .../core/schemas/utils/isPlainObject.js | 10 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../object/core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../unions/core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../core/schemas/builders/list/list.ts | 38 ++-- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 ++++++++++++------ .../core/schemas/builders/record/record.ts | 67 +++---- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- 570 files changed, 18305 insertions(+), 12699 deletions(-) diff --git a/seed/ts-express/alias-extends/core/schemas/builders/list/list.ts b/seed/ts-express/alias-extends/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/alias-extends/core/schemas/builders/list/list.ts +++ b/seed/ts-express/alias-extends/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/alias-extends/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/alias-extends/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/alias-extends/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/alias-extends/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/alias-extends/core/schemas/builders/object/object.ts b/seed/ts-express/alias-extends/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/alias-extends/core/schemas/builders/object/object.ts +++ b/seed/ts-express/alias-extends/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/alias-extends/core/schemas/builders/record/record.ts b/seed/ts-express/alias-extends/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/alias-extends/core/schemas/builders/record/record.ts +++ b/seed/ts-express/alias-extends/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/alias-extends/core/schemas/builders/union/union.ts b/seed/ts-express/alias-extends/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/alias-extends/core/schemas/builders/union/union.ts +++ b/seed/ts-express/alias-extends/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/alias-extends/core/schemas/utils/isPlainObject.ts b/seed/ts-express/alias-extends/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/alias-extends/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/alias-extends/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/alias/core/schemas/builders/list/list.ts b/seed/ts-express/alias/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/alias/core/schemas/builders/list/list.ts +++ b/seed/ts-express/alias/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/alias/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/alias/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/alias/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/alias/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/alias/core/schemas/builders/object/object.ts b/seed/ts-express/alias/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/alias/core/schemas/builders/object/object.ts +++ b/seed/ts-express/alias/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/alias/core/schemas/builders/record/record.ts b/seed/ts-express/alias/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/alias/core/schemas/builders/record/record.ts +++ b/seed/ts-express/alias/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/alias/core/schemas/builders/union/union.ts b/seed/ts-express/alias/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/alias/core/schemas/builders/union/union.ts +++ b/seed/ts-express/alias/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/alias/core/schemas/utils/isPlainObject.ts b/seed/ts-express/alias/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/alias/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/alias/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/any-auth/core/schemas/builders/list/list.ts b/seed/ts-express/any-auth/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/any-auth/core/schemas/builders/list/list.ts +++ b/seed/ts-express/any-auth/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/any-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/any-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/any-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/any-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/any-auth/core/schemas/builders/object/object.ts b/seed/ts-express/any-auth/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/any-auth/core/schemas/builders/object/object.ts +++ b/seed/ts-express/any-auth/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/any-auth/core/schemas/builders/record/record.ts b/seed/ts-express/any-auth/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/any-auth/core/schemas/builders/record/record.ts +++ b/seed/ts-express/any-auth/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/any-auth/core/schemas/builders/union/union.ts b/seed/ts-express/any-auth/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/any-auth/core/schemas/builders/union/union.ts +++ b/seed/ts-express/any-auth/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/any-auth/core/schemas/utils/isPlainObject.ts b/seed/ts-express/any-auth/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/any-auth/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/any-auth/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/audiences/core/schemas/builders/list/list.ts b/seed/ts-express/audiences/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/audiences/core/schemas/builders/list/list.ts +++ b/seed/ts-express/audiences/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/audiences/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/audiences/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/audiences/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/audiences/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/audiences/core/schemas/builders/object/object.ts b/seed/ts-express/audiences/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/audiences/core/schemas/builders/object/object.ts +++ b/seed/ts-express/audiences/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/audiences/core/schemas/builders/record/record.ts b/seed/ts-express/audiences/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/audiences/core/schemas/builders/record/record.ts +++ b/seed/ts-express/audiences/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/audiences/core/schemas/builders/union/union.ts b/seed/ts-express/audiences/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/audiences/core/schemas/builders/union/union.ts +++ b/seed/ts-express/audiences/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/audiences/core/schemas/utils/isPlainObject.ts b/seed/ts-express/audiences/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/audiences/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/audiences/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/list/list.ts b/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/list/list.ts +++ b/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/object/object.ts b/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/object/object.ts +++ b/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/record/record.ts b/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/record/record.ts +++ b/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/union/union.ts b/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/union/union.ts +++ b/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/basic-auth-environment-variables/core/schemas/utils/isPlainObject.ts b/seed/ts-express/basic-auth-environment-variables/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/basic-auth-environment-variables/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/basic-auth-environment-variables/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/basic-auth/core/schemas/builders/list/list.ts b/seed/ts-express/basic-auth/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/basic-auth/core/schemas/builders/list/list.ts +++ b/seed/ts-express/basic-auth/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/basic-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/basic-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/basic-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/basic-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/basic-auth/core/schemas/builders/object/object.ts b/seed/ts-express/basic-auth/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/basic-auth/core/schemas/builders/object/object.ts +++ b/seed/ts-express/basic-auth/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/basic-auth/core/schemas/builders/record/record.ts b/seed/ts-express/basic-auth/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/basic-auth/core/schemas/builders/record/record.ts +++ b/seed/ts-express/basic-auth/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/basic-auth/core/schemas/builders/union/union.ts b/seed/ts-express/basic-auth/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/basic-auth/core/schemas/builders/union/union.ts +++ b/seed/ts-express/basic-auth/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/basic-auth/core/schemas/utils/isPlainObject.ts b/seed/ts-express/basic-auth/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/basic-auth/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/basic-auth/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/list/list.ts b/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/list/list.ts +++ b/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/object/object.ts b/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/object/object.ts +++ b/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/record/record.ts b/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/record/record.ts +++ b/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/union/union.ts b/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/union/union.ts +++ b/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/bearer-token-environment-variable/core/schemas/utils/isPlainObject.ts b/seed/ts-express/bearer-token-environment-variable/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/bearer-token-environment-variable/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/bearer-token-environment-variable/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/circular-references-advanced/core/schemas/builders/list/list.ts b/seed/ts-express/circular-references-advanced/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/circular-references-advanced/core/schemas/builders/list/list.ts +++ b/seed/ts-express/circular-references-advanced/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/circular-references-advanced/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/circular-references-advanced/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/circular-references-advanced/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/circular-references-advanced/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/circular-references-advanced/core/schemas/builders/object/object.ts b/seed/ts-express/circular-references-advanced/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/circular-references-advanced/core/schemas/builders/object/object.ts +++ b/seed/ts-express/circular-references-advanced/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/circular-references-advanced/core/schemas/builders/record/record.ts b/seed/ts-express/circular-references-advanced/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/circular-references-advanced/core/schemas/builders/record/record.ts +++ b/seed/ts-express/circular-references-advanced/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/circular-references-advanced/core/schemas/builders/union/union.ts b/seed/ts-express/circular-references-advanced/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/circular-references-advanced/core/schemas/builders/union/union.ts +++ b/seed/ts-express/circular-references-advanced/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/circular-references-advanced/core/schemas/utils/isPlainObject.ts b/seed/ts-express/circular-references-advanced/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/circular-references-advanced/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/circular-references-advanced/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/circular-references/core/schemas/builders/list/list.ts b/seed/ts-express/circular-references/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/circular-references/core/schemas/builders/list/list.ts +++ b/seed/ts-express/circular-references/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/circular-references/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/circular-references/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/circular-references/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/circular-references/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/circular-references/core/schemas/builders/object/object.ts b/seed/ts-express/circular-references/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/circular-references/core/schemas/builders/object/object.ts +++ b/seed/ts-express/circular-references/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/circular-references/core/schemas/builders/record/record.ts b/seed/ts-express/circular-references/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/circular-references/core/schemas/builders/record/record.ts +++ b/seed/ts-express/circular-references/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/circular-references/core/schemas/builders/union/union.ts b/seed/ts-express/circular-references/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/circular-references/core/schemas/builders/union/union.ts +++ b/seed/ts-express/circular-references/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/circular-references/core/schemas/utils/isPlainObject.ts b/seed/ts-express/circular-references/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/circular-references/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/circular-references/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/client-side-params/core/schemas/builders/list/list.ts b/seed/ts-express/client-side-params/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/client-side-params/core/schemas/builders/list/list.ts +++ b/seed/ts-express/client-side-params/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/client-side-params/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/client-side-params/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/client-side-params/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/client-side-params/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/client-side-params/core/schemas/builders/object/object.ts b/seed/ts-express/client-side-params/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/client-side-params/core/schemas/builders/object/object.ts +++ b/seed/ts-express/client-side-params/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/client-side-params/core/schemas/builders/record/record.ts b/seed/ts-express/client-side-params/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/client-side-params/core/schemas/builders/record/record.ts +++ b/seed/ts-express/client-side-params/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/client-side-params/core/schemas/builders/union/union.ts b/seed/ts-express/client-side-params/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/client-side-params/core/schemas/builders/union/union.ts +++ b/seed/ts-express/client-side-params/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/client-side-params/core/schemas/utils/isPlainObject.ts b/seed/ts-express/client-side-params/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/client-side-params/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/client-side-params/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/content-type/core/schemas/builders/list/list.ts b/seed/ts-express/content-type/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/content-type/core/schemas/builders/list/list.ts +++ b/seed/ts-express/content-type/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/content-type/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/content-type/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/content-type/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/content-type/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/content-type/core/schemas/builders/object/object.ts b/seed/ts-express/content-type/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/content-type/core/schemas/builders/object/object.ts +++ b/seed/ts-express/content-type/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/content-type/core/schemas/builders/record/record.ts b/seed/ts-express/content-type/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/content-type/core/schemas/builders/record/record.ts +++ b/seed/ts-express/content-type/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/content-type/core/schemas/builders/union/union.ts b/seed/ts-express/content-type/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/content-type/core/schemas/builders/union/union.ts +++ b/seed/ts-express/content-type/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/content-type/core/schemas/utils/isPlainObject.ts b/seed/ts-express/content-type/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/content-type/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/content-type/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/cross-package-type-names/core/schemas/builders/list/list.ts b/seed/ts-express/cross-package-type-names/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/cross-package-type-names/core/schemas/builders/list/list.ts +++ b/seed/ts-express/cross-package-type-names/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/cross-package-type-names/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/cross-package-type-names/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/cross-package-type-names/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/cross-package-type-names/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/cross-package-type-names/core/schemas/builders/object/object.ts b/seed/ts-express/cross-package-type-names/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/cross-package-type-names/core/schemas/builders/object/object.ts +++ b/seed/ts-express/cross-package-type-names/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/cross-package-type-names/core/schemas/builders/record/record.ts b/seed/ts-express/cross-package-type-names/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/cross-package-type-names/core/schemas/builders/record/record.ts +++ b/seed/ts-express/cross-package-type-names/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/cross-package-type-names/core/schemas/builders/union/union.ts b/seed/ts-express/cross-package-type-names/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/cross-package-type-names/core/schemas/builders/union/union.ts +++ b/seed/ts-express/cross-package-type-names/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/cross-package-type-names/core/schemas/utils/isPlainObject.ts b/seed/ts-express/cross-package-type-names/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/cross-package-type-names/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/cross-package-type-names/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/dollar-string-examples/core/schemas/builders/list/list.ts b/seed/ts-express/dollar-string-examples/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/dollar-string-examples/core/schemas/builders/list/list.ts +++ b/seed/ts-express/dollar-string-examples/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/dollar-string-examples/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/dollar-string-examples/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/dollar-string-examples/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/dollar-string-examples/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/dollar-string-examples/core/schemas/builders/object/object.ts b/seed/ts-express/dollar-string-examples/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/dollar-string-examples/core/schemas/builders/object/object.ts +++ b/seed/ts-express/dollar-string-examples/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/dollar-string-examples/core/schemas/builders/record/record.ts b/seed/ts-express/dollar-string-examples/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/dollar-string-examples/core/schemas/builders/record/record.ts +++ b/seed/ts-express/dollar-string-examples/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/dollar-string-examples/core/schemas/builders/union/union.ts b/seed/ts-express/dollar-string-examples/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/dollar-string-examples/core/schemas/builders/union/union.ts +++ b/seed/ts-express/dollar-string-examples/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/dollar-string-examples/core/schemas/utils/isPlainObject.ts b/seed/ts-express/dollar-string-examples/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/dollar-string-examples/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/dollar-string-examples/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/empty-clients/core/schemas/builders/list/list.ts b/seed/ts-express/empty-clients/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/empty-clients/core/schemas/builders/list/list.ts +++ b/seed/ts-express/empty-clients/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/empty-clients/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/empty-clients/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/empty-clients/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/empty-clients/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/empty-clients/core/schemas/builders/object/object.ts b/seed/ts-express/empty-clients/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/empty-clients/core/schemas/builders/object/object.ts +++ b/seed/ts-express/empty-clients/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/empty-clients/core/schemas/builders/record/record.ts b/seed/ts-express/empty-clients/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/empty-clients/core/schemas/builders/record/record.ts +++ b/seed/ts-express/empty-clients/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/empty-clients/core/schemas/builders/union/union.ts b/seed/ts-express/empty-clients/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/empty-clients/core/schemas/builders/union/union.ts +++ b/seed/ts-express/empty-clients/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/empty-clients/core/schemas/utils/isPlainObject.ts b/seed/ts-express/empty-clients/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/empty-clients/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/empty-clients/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/endpoint-security-auth/core/schemas/builders/list/list.ts b/seed/ts-express/endpoint-security-auth/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/endpoint-security-auth/core/schemas/builders/list/list.ts +++ b/seed/ts-express/endpoint-security-auth/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/endpoint-security-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/endpoint-security-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/endpoint-security-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/endpoint-security-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/endpoint-security-auth/core/schemas/builders/object/object.ts b/seed/ts-express/endpoint-security-auth/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/endpoint-security-auth/core/schemas/builders/object/object.ts +++ b/seed/ts-express/endpoint-security-auth/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/endpoint-security-auth/core/schemas/builders/record/record.ts b/seed/ts-express/endpoint-security-auth/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/endpoint-security-auth/core/schemas/builders/record/record.ts +++ b/seed/ts-express/endpoint-security-auth/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/endpoint-security-auth/core/schemas/builders/union/union.ts b/seed/ts-express/endpoint-security-auth/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/endpoint-security-auth/core/schemas/builders/union/union.ts +++ b/seed/ts-express/endpoint-security-auth/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/endpoint-security-auth/core/schemas/utils/isPlainObject.ts b/seed/ts-express/endpoint-security-auth/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/endpoint-security-auth/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/endpoint-security-auth/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/error-property/core/schemas/builders/list/list.ts b/seed/ts-express/error-property/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/error-property/core/schemas/builders/list/list.ts +++ b/seed/ts-express/error-property/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/error-property/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/error-property/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/error-property/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/error-property/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/error-property/core/schemas/builders/object/object.ts b/seed/ts-express/error-property/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/error-property/core/schemas/builders/object/object.ts +++ b/seed/ts-express/error-property/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/error-property/core/schemas/builders/record/record.ts b/seed/ts-express/error-property/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/error-property/core/schemas/builders/record/record.ts +++ b/seed/ts-express/error-property/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/error-property/core/schemas/builders/union/union.ts b/seed/ts-express/error-property/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/error-property/core/schemas/builders/union/union.ts +++ b/seed/ts-express/error-property/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/error-property/core/schemas/utils/isPlainObject.ts b/seed/ts-express/error-property/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/error-property/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/error-property/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/errors/core/schemas/builders/list/list.ts b/seed/ts-express/errors/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/errors/core/schemas/builders/list/list.ts +++ b/seed/ts-express/errors/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/errors/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/errors/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/errors/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/errors/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/errors/core/schemas/builders/object/object.ts b/seed/ts-express/errors/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/errors/core/schemas/builders/object/object.ts +++ b/seed/ts-express/errors/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/errors/core/schemas/builders/record/record.ts b/seed/ts-express/errors/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/errors/core/schemas/builders/record/record.ts +++ b/seed/ts-express/errors/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/errors/core/schemas/builders/union/union.ts b/seed/ts-express/errors/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/errors/core/schemas/builders/union/union.ts +++ b/seed/ts-express/errors/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/errors/core/schemas/utils/isPlainObject.ts b/seed/ts-express/errors/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/errors/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/errors/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/examples/core/schemas/builders/list/list.ts b/seed/ts-express/examples/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/examples/core/schemas/builders/list/list.ts +++ b/seed/ts-express/examples/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/examples/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/examples/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/examples/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/examples/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/examples/core/schemas/builders/object/object.ts b/seed/ts-express/examples/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/examples/core/schemas/builders/object/object.ts +++ b/seed/ts-express/examples/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/examples/core/schemas/builders/record/record.ts b/seed/ts-express/examples/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/examples/core/schemas/builders/record/record.ts +++ b/seed/ts-express/examples/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/examples/core/schemas/builders/union/union.ts b/seed/ts-express/examples/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/examples/core/schemas/builders/union/union.ts +++ b/seed/ts-express/examples/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/examples/core/schemas/utils/isPlainObject.ts b/seed/ts-express/examples/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/examples/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/examples/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/list/list.ts b/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/list/list.ts +++ b/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/object/object.ts b/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/object/object.ts +++ b/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/record/record.ts b/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/record/record.ts +++ b/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/union/union.ts b/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/union/union.ts +++ b/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/utils/isPlainObject.ts b/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/list/list.ts b/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/list/list.ts +++ b/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/object/object.ts b/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/object/object.ts +++ b/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/record/record.ts b/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/record/record.ts +++ b/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/union/union.ts b/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/union/union.ts +++ b/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/exhaustive/no-custom-config/core/schemas/utils/isPlainObject.ts b/seed/ts-express/exhaustive/no-custom-config/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/exhaustive/no-custom-config/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/exhaustive/no-custom-config/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/list/list.ts b/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/list/list.ts +++ b/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/object/object.ts b/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/object/object.ts +++ b/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/record/record.ts b/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/record/record.ts +++ b/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/union/union.ts b/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/union/union.ts +++ b/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/exhaustive/no-optional-properties/core/schemas/utils/isPlainObject.ts b/seed/ts-express/exhaustive/no-optional-properties/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/exhaustive/no-optional-properties/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/exhaustive/no-optional-properties/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/list/list.ts b/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/list/list.ts +++ b/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/object/object.ts b/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/object/object.ts +++ b/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/record/record.ts b/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/record/record.ts +++ b/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/union/union.ts b/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/union/union.ts +++ b/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/exhaustive/retain-original-casing/core/schemas/utils/isPlainObject.ts b/seed/ts-express/exhaustive/retain-original-casing/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/exhaustive/retain-original-casing/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/exhaustive/retain-original-casing/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/list/list.ts b/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/list/list.ts +++ b/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/object/object.ts b/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/object/object.ts +++ b/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/record/record.ts b/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/record/record.ts +++ b/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/union/union.ts b/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/union/union.ts +++ b/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/utils/isPlainObject.ts b/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/exhaustive/union-utils/core/schemas/builders/list/list.ts b/seed/ts-express/exhaustive/union-utils/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/exhaustive/union-utils/core/schemas/builders/list/list.ts +++ b/seed/ts-express/exhaustive/union-utils/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/exhaustive/union-utils/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/exhaustive/union-utils/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/exhaustive/union-utils/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/exhaustive/union-utils/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/exhaustive/union-utils/core/schemas/builders/object/object.ts b/seed/ts-express/exhaustive/union-utils/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/exhaustive/union-utils/core/schemas/builders/object/object.ts +++ b/seed/ts-express/exhaustive/union-utils/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/exhaustive/union-utils/core/schemas/builders/record/record.ts b/seed/ts-express/exhaustive/union-utils/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/exhaustive/union-utils/core/schemas/builders/record/record.ts +++ b/seed/ts-express/exhaustive/union-utils/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/exhaustive/union-utils/core/schemas/builders/union/union.ts b/seed/ts-express/exhaustive/union-utils/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/exhaustive/union-utils/core/schemas/builders/union/union.ts +++ b/seed/ts-express/exhaustive/union-utils/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/exhaustive/union-utils/core/schemas/utils/isPlainObject.ts b/seed/ts-express/exhaustive/union-utils/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/exhaustive/union-utils/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/exhaustive/union-utils/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/extends/core/schemas/builders/list/list.ts b/seed/ts-express/extends/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/extends/core/schemas/builders/list/list.ts +++ b/seed/ts-express/extends/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/extends/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/extends/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/extends/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/extends/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/extends/core/schemas/builders/object/object.ts b/seed/ts-express/extends/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/extends/core/schemas/builders/object/object.ts +++ b/seed/ts-express/extends/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/extends/core/schemas/builders/record/record.ts b/seed/ts-express/extends/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/extends/core/schemas/builders/record/record.ts +++ b/seed/ts-express/extends/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/extends/core/schemas/builders/union/union.ts b/seed/ts-express/extends/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/extends/core/schemas/builders/union/union.ts +++ b/seed/ts-express/extends/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/extends/core/schemas/utils/isPlainObject.ts b/seed/ts-express/extends/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/extends/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/extends/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/extra-properties/core/schemas/builders/list/list.ts b/seed/ts-express/extra-properties/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/extra-properties/core/schemas/builders/list/list.ts +++ b/seed/ts-express/extra-properties/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/extra-properties/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/extra-properties/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/extra-properties/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/extra-properties/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/extra-properties/core/schemas/builders/object/object.ts b/seed/ts-express/extra-properties/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/extra-properties/core/schemas/builders/object/object.ts +++ b/seed/ts-express/extra-properties/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/extra-properties/core/schemas/builders/record/record.ts b/seed/ts-express/extra-properties/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/extra-properties/core/schemas/builders/record/record.ts +++ b/seed/ts-express/extra-properties/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/extra-properties/core/schemas/builders/union/union.ts b/seed/ts-express/extra-properties/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/extra-properties/core/schemas/builders/union/union.ts +++ b/seed/ts-express/extra-properties/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/extra-properties/core/schemas/utils/isPlainObject.ts b/seed/ts-express/extra-properties/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/extra-properties/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/extra-properties/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/folders/core/schemas/builders/list/list.ts b/seed/ts-express/folders/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/folders/core/schemas/builders/list/list.ts +++ b/seed/ts-express/folders/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/folders/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/folders/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/folders/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/folders/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/folders/core/schemas/builders/object/object.ts b/seed/ts-express/folders/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/folders/core/schemas/builders/object/object.ts +++ b/seed/ts-express/folders/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/folders/core/schemas/builders/record/record.ts b/seed/ts-express/folders/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/folders/core/schemas/builders/record/record.ts +++ b/seed/ts-express/folders/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/folders/core/schemas/builders/union/union.ts b/seed/ts-express/folders/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/folders/core/schemas/builders/union/union.ts +++ b/seed/ts-express/folders/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/folders/core/schemas/utils/isPlainObject.ts b/seed/ts-express/folders/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/folders/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/folders/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/header-auth-environment-variable/core/schemas/builders/list/list.ts b/seed/ts-express/header-auth-environment-variable/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/header-auth-environment-variable/core/schemas/builders/list/list.ts +++ b/seed/ts-express/header-auth-environment-variable/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/header-auth-environment-variable/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/header-auth-environment-variable/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/header-auth-environment-variable/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/header-auth-environment-variable/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/header-auth-environment-variable/core/schemas/builders/object/object.ts b/seed/ts-express/header-auth-environment-variable/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/header-auth-environment-variable/core/schemas/builders/object/object.ts +++ b/seed/ts-express/header-auth-environment-variable/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/header-auth-environment-variable/core/schemas/builders/record/record.ts b/seed/ts-express/header-auth-environment-variable/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/header-auth-environment-variable/core/schemas/builders/record/record.ts +++ b/seed/ts-express/header-auth-environment-variable/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/header-auth-environment-variable/core/schemas/builders/union/union.ts b/seed/ts-express/header-auth-environment-variable/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/header-auth-environment-variable/core/schemas/builders/union/union.ts +++ b/seed/ts-express/header-auth-environment-variable/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/header-auth-environment-variable/core/schemas/utils/isPlainObject.ts b/seed/ts-express/header-auth-environment-variable/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/header-auth-environment-variable/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/header-auth-environment-variable/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/header-auth/core/schemas/builders/list/list.ts b/seed/ts-express/header-auth/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/header-auth/core/schemas/builders/list/list.ts +++ b/seed/ts-express/header-auth/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/header-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/header-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/header-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/header-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/header-auth/core/schemas/builders/object/object.ts b/seed/ts-express/header-auth/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/header-auth/core/schemas/builders/object/object.ts +++ b/seed/ts-express/header-auth/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/header-auth/core/schemas/builders/record/record.ts b/seed/ts-express/header-auth/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/header-auth/core/schemas/builders/record/record.ts +++ b/seed/ts-express/header-auth/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/header-auth/core/schemas/builders/union/union.ts b/seed/ts-express/header-auth/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/header-auth/core/schemas/builders/union/union.ts +++ b/seed/ts-express/header-auth/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/header-auth/core/schemas/utils/isPlainObject.ts b/seed/ts-express/header-auth/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/header-auth/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/header-auth/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/http-head/core/schemas/builders/list/list.ts b/seed/ts-express/http-head/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/http-head/core/schemas/builders/list/list.ts +++ b/seed/ts-express/http-head/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/http-head/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/http-head/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/http-head/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/http-head/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/http-head/core/schemas/builders/object/object.ts b/seed/ts-express/http-head/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/http-head/core/schemas/builders/object/object.ts +++ b/seed/ts-express/http-head/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/http-head/core/schemas/builders/record/record.ts b/seed/ts-express/http-head/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/http-head/core/schemas/builders/record/record.ts +++ b/seed/ts-express/http-head/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/http-head/core/schemas/builders/union/union.ts b/seed/ts-express/http-head/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/http-head/core/schemas/builders/union/union.ts +++ b/seed/ts-express/http-head/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/http-head/core/schemas/utils/isPlainObject.ts b/seed/ts-express/http-head/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/http-head/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/http-head/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/idempotency-headers/core/schemas/builders/list/list.ts b/seed/ts-express/idempotency-headers/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/idempotency-headers/core/schemas/builders/list/list.ts +++ b/seed/ts-express/idempotency-headers/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/idempotency-headers/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/idempotency-headers/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/idempotency-headers/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/idempotency-headers/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/idempotency-headers/core/schemas/builders/object/object.ts b/seed/ts-express/idempotency-headers/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/idempotency-headers/core/schemas/builders/object/object.ts +++ b/seed/ts-express/idempotency-headers/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/idempotency-headers/core/schemas/builders/record/record.ts b/seed/ts-express/idempotency-headers/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/idempotency-headers/core/schemas/builders/record/record.ts +++ b/seed/ts-express/idempotency-headers/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/idempotency-headers/core/schemas/builders/union/union.ts b/seed/ts-express/idempotency-headers/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/idempotency-headers/core/schemas/builders/union/union.ts +++ b/seed/ts-express/idempotency-headers/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/idempotency-headers/core/schemas/utils/isPlainObject.ts b/seed/ts-express/idempotency-headers/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/idempotency-headers/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/idempotency-headers/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/imdb/no-custom-config/core/schemas/builders/list/list.ts b/seed/ts-express/imdb/no-custom-config/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/imdb/no-custom-config/core/schemas/builders/list/list.ts +++ b/seed/ts-express/imdb/no-custom-config/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/imdb/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/imdb/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/imdb/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/imdb/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/imdb/no-custom-config/core/schemas/builders/object/object.ts b/seed/ts-express/imdb/no-custom-config/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/imdb/no-custom-config/core/schemas/builders/object/object.ts +++ b/seed/ts-express/imdb/no-custom-config/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/imdb/no-custom-config/core/schemas/builders/record/record.ts b/seed/ts-express/imdb/no-custom-config/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/imdb/no-custom-config/core/schemas/builders/record/record.ts +++ b/seed/ts-express/imdb/no-custom-config/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/imdb/no-custom-config/core/schemas/builders/union/union.ts b/seed/ts-express/imdb/no-custom-config/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/imdb/no-custom-config/core/schemas/builders/union/union.ts +++ b/seed/ts-express/imdb/no-custom-config/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/imdb/no-custom-config/core/schemas/utils/isPlainObject.ts b/seed/ts-express/imdb/no-custom-config/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/imdb/no-custom-config/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/imdb/no-custom-config/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/imdb/output-compiled/core/schemas/builders/list/list.js b/seed/ts-express/imdb/output-compiled/core/schemas/builders/list/list.js index 464a6d952304..ac5f089dab78 100644 --- a/seed/ts-express/imdb/output-compiled/core/schemas/builders/list/list.js +++ b/seed/ts-express/imdb/output-compiled/core/schemas/builders/list/list.js @@ -31,24 +31,19 @@ function validateAndTransformArray(value, transformItem) { ], }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); - return maybeValidItems.reduce((acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; + const result = []; + const errors = []; + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); } - const errors = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { + else { errors.push(...item.errors); } - return { - ok: false, - errors, - }; - }, { ok: true, value: [] }); + } + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/imdb/output-compiled/core/schemas/builders/object-like/getObjectLikeUtils.js b/seed/ts-express/imdb/output-compiled/core/schemas/builders/object-like/getObjectLikeUtils.js index 2611c1f16dbe..41a2cbe41016 100644 --- a/seed/ts-express/imdb/output-compiled/core/schemas/builders/object-like/getObjectLikeUtils.js +++ b/seed/ts-express/imdb/output-compiled/core/schemas/builders/object-like/getObjectLikeUtils.js @@ -6,6 +6,8 @@ const filterObject_1 = require("../../utils/filterObject"); const getErrorMessageForIncorrectType_1 = require("../../utils/getErrorMessageForIncorrectType"); const isPlainObject_1 = require("../../utils/isPlainObject"); const index_1 = require("../schema-utils/index"); +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; function getObjectLikeUtils(schema) { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -21,9 +23,14 @@ function withParsedProperties(objectLike, properties) { if (!parsedObject.ok) { return parsedObject; } - const additionalProperties = Object.entries(properties).reduce((processed, [key, value]) => { - return Object.assign(Object.assign({}, processed), { [key]: typeof value === "function" ? value(parsedObject.value) : value }); - }, {}); + const additionalProperties = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key]; + additionalProperties[key] = + typeof value === "function" ? value(parsedObject.value) : value; + } + } return { ok: true, value: Object.assign(Object.assign({}, parsedObject.value), additionalProperties), diff --git a/seed/ts-express/imdb/output-compiled/core/schemas/builders/object/object.js b/seed/ts-express/imdb/output-compiled/core/schemas/builders/object/object.js index 97473ea31095..3ac05b5df1ab 100644 --- a/seed/ts-express/imdb/output-compiled/core/schemas/builders/object/object.js +++ b/seed/ts-express/imdb/output-compiled/core/schemas/builders/object/object.js @@ -13,63 +13,113 @@ const partition_1 = require("../../utils/partition"); const index_1 = require("../object-like/index"); const index_2 = require("../schema-utils/index"); const property_1 = require("./property"); +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; function object(schemas) { - const baseSchema = { - _getRawProperties: () => Object.entries(schemas).map(([parsedKey, propertySchema]) => (0, property_1.isProperty)(propertySchema) ? propertySchema.rawKey : parsedKey), - _getParsedProperties: () => (0, keys_1.keys)(schemas), - parse: (raw, opts) => { - const rawKeyToProperty = {}; - const requiredKeys = []; + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. + let _rawKeyToProperty; + function getRawKeyToProperty() { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of (0, entries_1.entries)(schemas)) { - const rawKey = (0, property_1.isProperty)(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = (0, property_1.isProperty)(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : parsedKey; const valueSchema = (0, property_1.isProperty)(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey, valueSchema, }; - rawKeyToProperty[rawKey] = property; + } + } + return _rawKeyToProperty; + } + let _parseRequiredKeys; + let _jsonRequiredKeys; + let _parseRequiredKeysSet; + let _jsonRequiredKeysSet; + function getParseRequiredKeys() { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of (0, entries_1.entries)(schemas)) { + const rawKey = (0, property_1.isProperty)(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : parsedKey; + const valueSchema = (0, property_1.isProperty)(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + function getJsonRequiredKeys() { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys; + } + function getParseRequiredKeysSet() { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet; + } + function getJsonRequiredKeysSet() { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet; + } + const baseSchema = { + _getRawProperties: () => Object.entries(schemas).map(([parsedKey, propertySchema]) => (0, property_1.isProperty)(propertySchema) ? propertySchema.rawKey : parsedKey), + _getParsedProperties: () => (0, keys_1.keys)(schemas), + parse: (raw, opts) => { + var _a; + const breadcrumbsPrefix = (_a = opts === null || opts === void 0 ? void 0 : opts.breadcrumbsPrefix) !== null && _a !== void 0 ? _a : []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, transform: (propertyValue) => { - var _a; - return property.valueSchema.parse(propertyValue, Object.assign(Object.assign({}, opts), { breadcrumbsPrefix: [...((_a = opts === null || opts === void 0 ? void 0 : opts.breadcrumbsPrefix) !== null && _a !== void 0 ? _a : []), rawKey] })); + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, Object.assign(Object.assign({}, opts), { breadcrumbsPrefix: childBreadcrumbs })); }, }; }, unrecognizedObjectKeys: opts === null || opts === void 0 ? void 0 : opts.unrecognizedObjectKeys, skipValidation: opts === null || opts === void 0 ? void 0 : opts.skipValidation, - breadcrumbsPrefix: opts === null || opts === void 0 ? void 0 : opts.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts === null || opts === void 0 ? void 0 : opts.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys = []; - for (const [parsedKey, schemaOrObjectProperty] of (0, entries_1.entries)(schemas)) { - const valueSchema = (0, property_1.isProperty)(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey); - } - } + var _a; + const breadcrumbsPrefix = (_a = opts === null || opts === void 0 ? void 0 : opts.breadcrumbsPrefix) !== null && _a !== void 0 ? _a : []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: (parsedKey) => { const property = schemas[parsedKey]; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition @@ -80,8 +130,8 @@ function object(schemas) { return { transformedKey: property.rawKey, transform: (propertyValue) => { - var _a; - return property.valueSchema.json(propertyValue, Object.assign(Object.assign({}, opts), { breadcrumbsPrefix: [...((_a = opts === null || opts === void 0 ? void 0 : opts.breadcrumbsPrefix) !== null && _a !== void 0 ? _a : []), parsedKey] })); + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, Object.assign(Object.assign({}, opts), { breadcrumbsPrefix: childBreadcrumbs })); }, }; } @@ -89,15 +139,15 @@ function object(schemas) { return { transformedKey: parsedKey, transform: (propertyValue) => { - var _a; - return property.json(propertyValue, Object.assign(Object.assign({}, opts), { breadcrumbsPrefix: [...((_a = opts === null || opts === void 0 ? void 0 : opts.breadcrumbsPrefix) !== null && _a !== void 0 ? _a : []), parsedKey] })); + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, Object.assign(Object.assign({}, opts), { breadcrumbsPrefix: childBreadcrumbs })); }, }; } }, unrecognizedObjectKeys: opts === null || opts === void 0 ? void 0 : opts.unrecognizedObjectKeys, skipValidation: opts === null || opts === void 0 ? void 0 : opts.skipValidation, - breadcrumbsPrefix: opts === null || opts === void 0 ? void 0 : opts.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts === null || opts === void 0 ? void 0 : opts.omitUndefined, }); }, @@ -105,7 +155,7 @@ function object(schemas) { }; return Object.assign(Object.assign(Object.assign(Object.assign({}, (0, maybeSkipValidation_1.maybeSkipValidation)(baseSchema)), (0, index_2.getSchemaUtils)(baseSchema)), (0, index_1.getObjectLikeUtils)(baseSchema)), getObjectUtils(baseSchema)); } -function validateAndTransformObject({ value, requiredKeys, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, breadcrumbsPrefix = [], }) { +function validateAndTransformObject({ value, requiredKeys, requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, breadcrumbsPrefix = [], }) { if (!(0, isPlainObject_1.isPlainObject)(value)) { return { ok: false, @@ -117,13 +167,21 @@ function validateAndTransformObject({ value, requiredKeys, getProperty, unrecogn ], }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors = []; const transformed = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue); if (value.ok) { transformed[property.transformedKey] = value.value; @@ -149,12 +207,16 @@ function validateAndTransformObject({ value, requiredKeys, getProperty, unrecogn } } } - errors.push(...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - }))); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in value)) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { ok: true, diff --git a/seed/ts-express/imdb/output-compiled/core/schemas/builders/record/record.js b/seed/ts-express/imdb/output-compiled/core/schemas/builders/record/record.js index 773966c688ea..9ea69d74a431 100644 --- a/seed/ts-express/imdb/output-compiled/core/schemas/builders/record/record.js +++ b/seed/ts-express/imdb/output-compiled/core/schemas/builders/record/record.js @@ -2,11 +2,12 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.record = record; const Schema_1 = require("../../Schema"); -const entries_1 = require("../../utils/entries"); const getErrorMessageForIncorrectType_1 = require("../../utils/getErrorMessageForIncorrectType"); const isPlainObject_1 = require("../../utils/isPlainObject"); const maybeSkipValidation_1 = require("../../utils/maybeSkipValidation"); const index_1 = require("../schema-utils/index"); +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; function record(keySchema, valueSchema) { const baseSchema = { parse: (raw, opts) => { @@ -55,11 +56,16 @@ function validateAndTransformRecord({ value, isKeyNumeric, transformKey, transfo ], }; } - return (0, entries_1.entries)(value).reduce((accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; + const result = {}; + const errors = []; + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = value[stringKey]; + if (entryValue === undefined) { + continue; } - const acc = accPromise; let key = stringKey; if (isKeyNumeric) { const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; @@ -68,26 +74,21 @@ function validateAndTransformRecord({ value, isKeyNumeric, transformKey, transfo } } const transformedKey = transformKey(key); - const transformedValue = transformValue(value, key); - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: Object.assign(Object.assign({}, acc.value), { [transformedKey.value]: transformedValue.value }), - }; - } - const errors = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!transformedKey.ok) { - errors.push(...transformedKey.errors); + const transformedValue = transformValue(entryValue, key); + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; } - if (!transformedValue.ok) { - errors.push(...transformedValue.errors); + else { + if (!transformedKey.ok) { + errors.push(...transformedKey.errors); + } + if (!transformedValue.ok) { + errors.push(...transformedValue.errors); + } } - return { - ok: false, - errors, - }; - }, { ok: true, value: {} }); + } + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/imdb/output-compiled/core/schemas/builders/union/union.js b/seed/ts-express/imdb/output-compiled/core/schemas/builders/union/union.js index f608f67ed6f8..24a56120e042 100644 --- a/seed/ts-express/imdb/output-compiled/core/schemas/builders/union/union.js +++ b/seed/ts-express/imdb/output-compiled/core/schemas/builders/union/union.js @@ -1,15 +1,4 @@ "use strict"; -var __rest = (this && this.__rest) || function (s, e) { - var t = {}; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) - t[p] = s[p]; - if (s != null && typeof Object.getOwnPropertySymbols === "function") - for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { - if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) - t[p[i]] = s[p[i]]; - } - return t; -}; Object.defineProperty(exports, "__esModule", { value: true }); exports.union = union; const Schema_1 = require("../../Schema"); @@ -20,6 +9,8 @@ const maybeSkipValidation_1 = require("../../utils/maybeSkipValidation"); const index_1 = require("../enum/index"); const object_like_1 = require("../object-like"); const index_2 = require("../schema-utils/index"); +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; function union(discriminant, union) { const rawDiscriminant = typeof discriminant === "string" ? discriminant : discriminant.rawDiscriminant; const parsedDiscriminant = typeof discriminant === "string" @@ -79,7 +70,13 @@ function transformAndValidateUnion({ value, discriminant, transformedDiscriminan ], }; } - const _a = value, _b = discriminant, discriminantValue = _a[_b], additionalProperties = __rest(_a, [typeof _b === "symbol" ? _b : _b + ""]); + const discriminantValue = value[discriminant]; + const additionalProperties = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { ok: false, diff --git a/seed/ts-express/imdb/output-compiled/core/schemas/utils/isPlainObject.js b/seed/ts-express/imdb/output-compiled/core/schemas/utils/isPlainObject.js index 6b270a480ae9..5f1a6a1d015d 100644 --- a/seed/ts-express/imdb/output-compiled/core/schemas/utils/isPlainObject.js +++ b/seed/ts-express/imdb/output-compiled/core/schemas/utils/isPlainObject.js @@ -6,12 +6,10 @@ function isPlainObject(value) { if (typeof value !== "object" || value === null) { return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/imdb/output-src-only/core/schemas/builders/list/list.ts b/seed/ts-express/imdb/output-src-only/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/imdb/output-src-only/core/schemas/builders/list/list.ts +++ b/seed/ts-express/imdb/output-src-only/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/imdb/output-src-only/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/imdb/output-src-only/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/imdb/output-src-only/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/imdb/output-src-only/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/imdb/output-src-only/core/schemas/builders/object/object.ts b/seed/ts-express/imdb/output-src-only/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/imdb/output-src-only/core/schemas/builders/object/object.ts +++ b/seed/ts-express/imdb/output-src-only/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/imdb/output-src-only/core/schemas/builders/record/record.ts b/seed/ts-express/imdb/output-src-only/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/imdb/output-src-only/core/schemas/builders/record/record.ts +++ b/seed/ts-express/imdb/output-src-only/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/imdb/output-src-only/core/schemas/builders/union/union.ts b/seed/ts-express/imdb/output-src-only/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/imdb/output-src-only/core/schemas/builders/union/union.ts +++ b/seed/ts-express/imdb/output-src-only/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/imdb/output-src-only/core/schemas/utils/isPlainObject.ts b/seed/ts-express/imdb/output-src-only/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/imdb/output-src-only/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/imdb/output-src-only/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/list/list.ts b/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/list/list.ts +++ b/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/object/object.ts b/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/object/object.ts +++ b/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/record/record.ts b/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/record/record.ts +++ b/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/union/union.ts b/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/union/union.ts +++ b/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/imdb/skip-request-validation/core/schemas/utils/isPlainObject.ts b/seed/ts-express/imdb/skip-request-validation/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/imdb/skip-request-validation/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/imdb/skip-request-validation/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/list/list.ts b/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/list/list.ts +++ b/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/object/object.ts b/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/object/object.ts +++ b/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/record/record.ts b/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/record/record.ts +++ b/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/union/union.ts b/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/union/union.ts +++ b/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/imdb/skip-response-validation/core/schemas/utils/isPlainObject.ts b/seed/ts-express/imdb/skip-response-validation/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/imdb/skip-response-validation/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/imdb/skip-response-validation/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/imdb/validation-status-code/core/schemas/builders/list/list.ts b/seed/ts-express/imdb/validation-status-code/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/imdb/validation-status-code/core/schemas/builders/list/list.ts +++ b/seed/ts-express/imdb/validation-status-code/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/imdb/validation-status-code/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/imdb/validation-status-code/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/imdb/validation-status-code/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/imdb/validation-status-code/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/imdb/validation-status-code/core/schemas/builders/object/object.ts b/seed/ts-express/imdb/validation-status-code/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/imdb/validation-status-code/core/schemas/builders/object/object.ts +++ b/seed/ts-express/imdb/validation-status-code/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/imdb/validation-status-code/core/schemas/builders/record/record.ts b/seed/ts-express/imdb/validation-status-code/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/imdb/validation-status-code/core/schemas/builders/record/record.ts +++ b/seed/ts-express/imdb/validation-status-code/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/imdb/validation-status-code/core/schemas/builders/union/union.ts b/seed/ts-express/imdb/validation-status-code/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/imdb/validation-status-code/core/schemas/builders/union/union.ts +++ b/seed/ts-express/imdb/validation-status-code/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/imdb/validation-status-code/core/schemas/utils/isPlainObject.ts b/seed/ts-express/imdb/validation-status-code/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/imdb/validation-status-code/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/imdb/validation-status-code/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/inferred-auth-explicit/core/schemas/builders/list/list.ts b/seed/ts-express/inferred-auth-explicit/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/inferred-auth-explicit/core/schemas/builders/list/list.ts +++ b/seed/ts-express/inferred-auth-explicit/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/inferred-auth-explicit/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/inferred-auth-explicit/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/inferred-auth-explicit/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/inferred-auth-explicit/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/inferred-auth-explicit/core/schemas/builders/object/object.ts b/seed/ts-express/inferred-auth-explicit/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/inferred-auth-explicit/core/schemas/builders/object/object.ts +++ b/seed/ts-express/inferred-auth-explicit/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/inferred-auth-explicit/core/schemas/builders/record/record.ts b/seed/ts-express/inferred-auth-explicit/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/inferred-auth-explicit/core/schemas/builders/record/record.ts +++ b/seed/ts-express/inferred-auth-explicit/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/inferred-auth-explicit/core/schemas/builders/union/union.ts b/seed/ts-express/inferred-auth-explicit/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/inferred-auth-explicit/core/schemas/builders/union/union.ts +++ b/seed/ts-express/inferred-auth-explicit/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/inferred-auth-explicit/core/schemas/utils/isPlainObject.ts b/seed/ts-express/inferred-auth-explicit/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/inferred-auth-explicit/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/inferred-auth-explicit/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/list/list.ts b/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/list/list.ts +++ b/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/object/object.ts b/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/object/object.ts +++ b/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/record/record.ts b/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/record/record.ts +++ b/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/union/union.ts b/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/union/union.ts +++ b/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/utils/isPlainObject.ts b/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/list/list.ts b/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/list/list.ts +++ b/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/object/object.ts b/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/object/object.ts +++ b/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/record/record.ts b/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/record/record.ts +++ b/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/union/union.ts b/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/union/union.ts +++ b/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/utils/isPlainObject.ts b/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/list/list.ts b/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/list/list.ts +++ b/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/object/object.ts b/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/object/object.ts +++ b/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/record/record.ts b/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/record/record.ts +++ b/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/union/union.ts b/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/union/union.ts +++ b/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/inferred-auth-implicit-reference/core/schemas/utils/isPlainObject.ts b/seed/ts-express/inferred-auth-implicit-reference/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/inferred-auth-implicit-reference/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/inferred-auth-implicit-reference/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/inferred-auth-implicit/core/schemas/builders/list/list.ts b/seed/ts-express/inferred-auth-implicit/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/inferred-auth-implicit/core/schemas/builders/list/list.ts +++ b/seed/ts-express/inferred-auth-implicit/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/inferred-auth-implicit/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/inferred-auth-implicit/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/inferred-auth-implicit/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/inferred-auth-implicit/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/inferred-auth-implicit/core/schemas/builders/object/object.ts b/seed/ts-express/inferred-auth-implicit/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/inferred-auth-implicit/core/schemas/builders/object/object.ts +++ b/seed/ts-express/inferred-auth-implicit/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/inferred-auth-implicit/core/schemas/builders/record/record.ts b/seed/ts-express/inferred-auth-implicit/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/inferred-auth-implicit/core/schemas/builders/record/record.ts +++ b/seed/ts-express/inferred-auth-implicit/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/inferred-auth-implicit/core/schemas/builders/union/union.ts b/seed/ts-express/inferred-auth-implicit/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/inferred-auth-implicit/core/schemas/builders/union/union.ts +++ b/seed/ts-express/inferred-auth-implicit/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/inferred-auth-implicit/core/schemas/utils/isPlainObject.ts b/seed/ts-express/inferred-auth-implicit/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/inferred-auth-implicit/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/inferred-auth-implicit/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/license/core/schemas/builders/list/list.ts b/seed/ts-express/license/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/license/core/schemas/builders/list/list.ts +++ b/seed/ts-express/license/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/license/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/license/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/license/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/license/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/license/core/schemas/builders/object/object.ts b/seed/ts-express/license/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/license/core/schemas/builders/object/object.ts +++ b/seed/ts-express/license/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/license/core/schemas/builders/record/record.ts b/seed/ts-express/license/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/license/core/schemas/builders/record/record.ts +++ b/seed/ts-express/license/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/license/core/schemas/builders/union/union.ts b/seed/ts-express/license/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/license/core/schemas/builders/union/union.ts +++ b/seed/ts-express/license/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/license/core/schemas/utils/isPlainObject.ts b/seed/ts-express/license/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/license/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/license/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/literal/core/schemas/builders/list/list.ts b/seed/ts-express/literal/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/literal/core/schemas/builders/list/list.ts +++ b/seed/ts-express/literal/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/literal/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/literal/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/literal/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/literal/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/literal/core/schemas/builders/object/object.ts b/seed/ts-express/literal/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/literal/core/schemas/builders/object/object.ts +++ b/seed/ts-express/literal/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/literal/core/schemas/builders/record/record.ts b/seed/ts-express/literal/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/literal/core/schemas/builders/record/record.ts +++ b/seed/ts-express/literal/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/literal/core/schemas/builders/union/union.ts b/seed/ts-express/literal/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/literal/core/schemas/builders/union/union.ts +++ b/seed/ts-express/literal/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/literal/core/schemas/utils/isPlainObject.ts b/seed/ts-express/literal/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/literal/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/literal/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/literals-unions/core/schemas/builders/list/list.ts b/seed/ts-express/literals-unions/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/literals-unions/core/schemas/builders/list/list.ts +++ b/seed/ts-express/literals-unions/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/literals-unions/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/literals-unions/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/literals-unions/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/literals-unions/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/literals-unions/core/schemas/builders/object/object.ts b/seed/ts-express/literals-unions/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/literals-unions/core/schemas/builders/object/object.ts +++ b/seed/ts-express/literals-unions/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/literals-unions/core/schemas/builders/record/record.ts b/seed/ts-express/literals-unions/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/literals-unions/core/schemas/builders/record/record.ts +++ b/seed/ts-express/literals-unions/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/literals-unions/core/schemas/builders/union/union.ts b/seed/ts-express/literals-unions/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/literals-unions/core/schemas/builders/union/union.ts +++ b/seed/ts-express/literals-unions/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/literals-unions/core/schemas/utils/isPlainObject.ts b/seed/ts-express/literals-unions/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/literals-unions/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/literals-unions/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/list/list.ts b/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/list/list.ts +++ b/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/object/object.ts b/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/object/object.ts +++ b/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/record/record.ts b/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/record/record.ts +++ b/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/union/union.ts b/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/union/union.ts +++ b/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/mixed-case/no-custom-config/core/schemas/utils/isPlainObject.ts b/seed/ts-express/mixed-case/no-custom-config/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/mixed-case/no-custom-config/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/mixed-case/no-custom-config/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/list/list.ts b/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/list/list.ts +++ b/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/object/object.ts b/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/object/object.ts +++ b/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/record/record.ts b/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/record/record.ts +++ b/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/union/union.ts b/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/union/union.ts +++ b/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/mixed-case/retain-original-casing/core/schemas/utils/isPlainObject.ts b/seed/ts-express/mixed-case/retain-original-casing/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/mixed-case/retain-original-casing/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/mixed-case/retain-original-casing/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/list/list.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/mixed-file-directory/core/schemas/builders/list/list.ts +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/mixed-file-directory/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/object/object.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/mixed-file-directory/core/schemas/builders/object/object.ts +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/record/record.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/mixed-file-directory/core/schemas/builders/record/record.ts +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/union/union.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/mixed-file-directory/core/schemas/builders/union/union.ts +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/mixed-file-directory/core/schemas/utils/isPlainObject.ts b/seed/ts-express/mixed-file-directory/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/mixed-file-directory/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/mixed-file-directory/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/multi-line-docs/core/schemas/builders/list/list.ts b/seed/ts-express/multi-line-docs/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/multi-line-docs/core/schemas/builders/list/list.ts +++ b/seed/ts-express/multi-line-docs/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/multi-line-docs/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/multi-line-docs/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/multi-line-docs/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/multi-line-docs/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/multi-line-docs/core/schemas/builders/object/object.ts b/seed/ts-express/multi-line-docs/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/multi-line-docs/core/schemas/builders/object/object.ts +++ b/seed/ts-express/multi-line-docs/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/multi-line-docs/core/schemas/builders/record/record.ts b/seed/ts-express/multi-line-docs/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/multi-line-docs/core/schemas/builders/record/record.ts +++ b/seed/ts-express/multi-line-docs/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/multi-line-docs/core/schemas/builders/union/union.ts b/seed/ts-express/multi-line-docs/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/multi-line-docs/core/schemas/builders/union/union.ts +++ b/seed/ts-express/multi-line-docs/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/multi-line-docs/core/schemas/utils/isPlainObject.ts b/seed/ts-express/multi-line-docs/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/multi-line-docs/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/multi-line-docs/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/list/list.ts b/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/list/list.ts +++ b/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/object/object.ts b/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/object/object.ts +++ b/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/record/record.ts b/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/record/record.ts +++ b/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/union/union.ts b/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/union/union.ts +++ b/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/multi-url-environment-no-default/core/schemas/utils/isPlainObject.ts b/seed/ts-express/multi-url-environment-no-default/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/multi-url-environment-no-default/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/multi-url-environment-no-default/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/multi-url-environment/core/schemas/builders/list/list.ts b/seed/ts-express/multi-url-environment/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/multi-url-environment/core/schemas/builders/list/list.ts +++ b/seed/ts-express/multi-url-environment/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/multi-url-environment/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/multi-url-environment/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/multi-url-environment/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/multi-url-environment/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/multi-url-environment/core/schemas/builders/object/object.ts b/seed/ts-express/multi-url-environment/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/multi-url-environment/core/schemas/builders/object/object.ts +++ b/seed/ts-express/multi-url-environment/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/multi-url-environment/core/schemas/builders/record/record.ts b/seed/ts-express/multi-url-environment/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/multi-url-environment/core/schemas/builders/record/record.ts +++ b/seed/ts-express/multi-url-environment/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/multi-url-environment/core/schemas/builders/union/union.ts b/seed/ts-express/multi-url-environment/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/multi-url-environment/core/schemas/builders/union/union.ts +++ b/seed/ts-express/multi-url-environment/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/multi-url-environment/core/schemas/utils/isPlainObject.ts b/seed/ts-express/multi-url-environment/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/multi-url-environment/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/multi-url-environment/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/no-environment/core/schemas/builders/list/list.ts b/seed/ts-express/no-environment/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/no-environment/core/schemas/builders/list/list.ts +++ b/seed/ts-express/no-environment/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/no-environment/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/no-environment/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/no-environment/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/no-environment/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/no-environment/core/schemas/builders/object/object.ts b/seed/ts-express/no-environment/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/no-environment/core/schemas/builders/object/object.ts +++ b/seed/ts-express/no-environment/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/no-environment/core/schemas/builders/record/record.ts b/seed/ts-express/no-environment/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/no-environment/core/schemas/builders/record/record.ts +++ b/seed/ts-express/no-environment/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/no-environment/core/schemas/builders/union/union.ts b/seed/ts-express/no-environment/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/no-environment/core/schemas/builders/union/union.ts +++ b/seed/ts-express/no-environment/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/no-environment/core/schemas/utils/isPlainObject.ts b/seed/ts-express/no-environment/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/no-environment/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/no-environment/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/no-retries/core/schemas/builders/list/list.ts b/seed/ts-express/no-retries/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/no-retries/core/schemas/builders/list/list.ts +++ b/seed/ts-express/no-retries/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/no-retries/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/no-retries/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/no-retries/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/no-retries/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/no-retries/core/schemas/builders/object/object.ts b/seed/ts-express/no-retries/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/no-retries/core/schemas/builders/object/object.ts +++ b/seed/ts-express/no-retries/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/no-retries/core/schemas/builders/record/record.ts b/seed/ts-express/no-retries/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/no-retries/core/schemas/builders/record/record.ts +++ b/seed/ts-express/no-retries/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/no-retries/core/schemas/builders/union/union.ts b/seed/ts-express/no-retries/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/no-retries/core/schemas/builders/union/union.ts +++ b/seed/ts-express/no-retries/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/no-retries/core/schemas/utils/isPlainObject.ts b/seed/ts-express/no-retries/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/no-retries/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/no-retries/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/nullable-allof-extends/core/schemas/builders/list/list.ts b/seed/ts-express/nullable-allof-extends/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/nullable-allof-extends/core/schemas/builders/list/list.ts +++ b/seed/ts-express/nullable-allof-extends/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/nullable-allof-extends/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/nullable-allof-extends/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/nullable-allof-extends/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/nullable-allof-extends/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/nullable-allof-extends/core/schemas/builders/object/object.ts b/seed/ts-express/nullable-allof-extends/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/nullable-allof-extends/core/schemas/builders/object/object.ts +++ b/seed/ts-express/nullable-allof-extends/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/nullable-allof-extends/core/schemas/builders/record/record.ts b/seed/ts-express/nullable-allof-extends/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/nullable-allof-extends/core/schemas/builders/record/record.ts +++ b/seed/ts-express/nullable-allof-extends/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/nullable-allof-extends/core/schemas/builders/union/union.ts b/seed/ts-express/nullable-allof-extends/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/nullable-allof-extends/core/schemas/builders/union/union.ts +++ b/seed/ts-express/nullable-allof-extends/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/nullable-allof-extends/core/schemas/utils/isPlainObject.ts b/seed/ts-express/nullable-allof-extends/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/nullable-allof-extends/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/nullable-allof-extends/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/nullable-optional/core/schemas/builders/list/list.ts b/seed/ts-express/nullable-optional/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/nullable-optional/core/schemas/builders/list/list.ts +++ b/seed/ts-express/nullable-optional/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/nullable-optional/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/nullable-optional/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/nullable-optional/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/nullable-optional/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/nullable-optional/core/schemas/builders/object/object.ts b/seed/ts-express/nullable-optional/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/nullable-optional/core/schemas/builders/object/object.ts +++ b/seed/ts-express/nullable-optional/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/nullable-optional/core/schemas/builders/record/record.ts b/seed/ts-express/nullable-optional/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/nullable-optional/core/schemas/builders/record/record.ts +++ b/seed/ts-express/nullable-optional/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/nullable-optional/core/schemas/builders/union/union.ts b/seed/ts-express/nullable-optional/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/nullable-optional/core/schemas/builders/union/union.ts +++ b/seed/ts-express/nullable-optional/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/nullable-optional/core/schemas/utils/isPlainObject.ts b/seed/ts-express/nullable-optional/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/nullable-optional/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/nullable-optional/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/nullable-request-body/core/schemas/builders/list/list.ts b/seed/ts-express/nullable-request-body/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/nullable-request-body/core/schemas/builders/list/list.ts +++ b/seed/ts-express/nullable-request-body/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/nullable-request-body/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/nullable-request-body/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/nullable-request-body/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/nullable-request-body/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/nullable-request-body/core/schemas/builders/object/object.ts b/seed/ts-express/nullable-request-body/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/nullable-request-body/core/schemas/builders/object/object.ts +++ b/seed/ts-express/nullable-request-body/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/nullable-request-body/core/schemas/builders/record/record.ts b/seed/ts-express/nullable-request-body/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/nullable-request-body/core/schemas/builders/record/record.ts +++ b/seed/ts-express/nullable-request-body/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/nullable-request-body/core/schemas/builders/union/union.ts b/seed/ts-express/nullable-request-body/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/nullable-request-body/core/schemas/builders/union/union.ts +++ b/seed/ts-express/nullable-request-body/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/nullable-request-body/core/schemas/utils/isPlainObject.ts b/seed/ts-express/nullable-request-body/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/nullable-request-body/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/nullable-request-body/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/nullable/core/schemas/builders/list/list.ts b/seed/ts-express/nullable/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/nullable/core/schemas/builders/list/list.ts +++ b/seed/ts-express/nullable/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/nullable/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/nullable/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/nullable/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/nullable/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/nullable/core/schemas/builders/object/object.ts b/seed/ts-express/nullable/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/nullable/core/schemas/builders/object/object.ts +++ b/seed/ts-express/nullable/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/nullable/core/schemas/builders/record/record.ts b/seed/ts-express/nullable/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/nullable/core/schemas/builders/record/record.ts +++ b/seed/ts-express/nullable/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/nullable/core/schemas/builders/union/union.ts b/seed/ts-express/nullable/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/nullable/core/schemas/builders/union/union.ts +++ b/seed/ts-express/nullable/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/nullable/core/schemas/utils/isPlainObject.ts b/seed/ts-express/nullable/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/nullable/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/nullable/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/list/list.ts b/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/list/list.ts +++ b/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/object/object.ts b/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/object/object.ts +++ b/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/record/record.ts b/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/record/record.ts +++ b/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/union/union.ts b/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/union/union.ts +++ b/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/oauth-client-credentials-custom/core/schemas/utils/isPlainObject.ts b/seed/ts-express/oauth-client-credentials-custom/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/oauth-client-credentials-custom/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/oauth-client-credentials-custom/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/list/list.ts b/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/list/list.ts +++ b/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/object/object.ts b/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/object/object.ts +++ b/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/record/record.ts b/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/record/record.ts +++ b/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/union/union.ts b/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/union/union.ts +++ b/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/oauth-client-credentials-default/core/schemas/utils/isPlainObject.ts b/seed/ts-express/oauth-client-credentials-default/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/oauth-client-credentials-default/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/oauth-client-credentials-default/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/list/list.ts b/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/list/list.ts +++ b/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/object/object.ts b/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/object/object.ts +++ b/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/record/record.ts b/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/record/record.ts +++ b/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/union/union.ts b/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/union/union.ts +++ b/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/utils/isPlainObject.ts b/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/list/list.ts b/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/list/list.ts +++ b/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/object/object.ts b/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/object/object.ts +++ b/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/record/record.ts b/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/record/record.ts +++ b/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/union/union.ts b/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/union/union.ts +++ b/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/utils/isPlainObject.ts b/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/list/list.ts b/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/list/list.ts +++ b/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/object/object.ts b/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/object/object.ts +++ b/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/record/record.ts b/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/record/record.ts +++ b/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/union/union.ts b/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/union/union.ts +++ b/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/utils/isPlainObject.ts b/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/list/list.ts b/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/list/list.ts +++ b/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/object/object.ts b/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/object/object.ts +++ b/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/record/record.ts b/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/record/record.ts +++ b/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/union/union.ts b/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/union/union.ts +++ b/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/oauth-client-credentials-reference/core/schemas/utils/isPlainObject.ts b/seed/ts-express/oauth-client-credentials-reference/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/oauth-client-credentials-reference/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/oauth-client-credentials-reference/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/list/list.ts b/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/list/list.ts +++ b/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/object/object.ts b/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/object/object.ts +++ b/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/record/record.ts b/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/record/record.ts +++ b/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/union/union.ts b/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/union/union.ts +++ b/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/utils/isPlainObject.ts b/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/oauth-client-credentials/core/schemas/builders/list/list.ts b/seed/ts-express/oauth-client-credentials/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/oauth-client-credentials/core/schemas/builders/list/list.ts +++ b/seed/ts-express/oauth-client-credentials/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/oauth-client-credentials/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/oauth-client-credentials/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/oauth-client-credentials/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/oauth-client-credentials/core/schemas/builders/object/object.ts b/seed/ts-express/oauth-client-credentials/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/oauth-client-credentials/core/schemas/builders/object/object.ts +++ b/seed/ts-express/oauth-client-credentials/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/oauth-client-credentials/core/schemas/builders/record/record.ts b/seed/ts-express/oauth-client-credentials/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/oauth-client-credentials/core/schemas/builders/record/record.ts +++ b/seed/ts-express/oauth-client-credentials/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials/core/schemas/builders/union/union.ts b/seed/ts-express/oauth-client-credentials/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/oauth-client-credentials/core/schemas/builders/union/union.ts +++ b/seed/ts-express/oauth-client-credentials/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/oauth-client-credentials/core/schemas/utils/isPlainObject.ts b/seed/ts-express/oauth-client-credentials/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/oauth-client-credentials/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/oauth-client-credentials/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/object/core/schemas/builders/list/list.ts b/seed/ts-express/object/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/object/core/schemas/builders/list/list.ts +++ b/seed/ts-express/object/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/object/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/object/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/object/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/object/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/object/core/schemas/builders/object/object.ts b/seed/ts-express/object/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/object/core/schemas/builders/object/object.ts +++ b/seed/ts-express/object/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/object/core/schemas/builders/record/record.ts b/seed/ts-express/object/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/object/core/schemas/builders/record/record.ts +++ b/seed/ts-express/object/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/object/core/schemas/builders/union/union.ts b/seed/ts-express/object/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/object/core/schemas/builders/union/union.ts +++ b/seed/ts-express/object/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/object/core/schemas/utils/isPlainObject.ts b/seed/ts-express/object/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/object/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/object/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/objects-with-imports/core/schemas/builders/list/list.ts b/seed/ts-express/objects-with-imports/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/objects-with-imports/core/schemas/builders/list/list.ts +++ b/seed/ts-express/objects-with-imports/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/objects-with-imports/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/objects-with-imports/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/objects-with-imports/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/objects-with-imports/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/objects-with-imports/core/schemas/builders/object/object.ts b/seed/ts-express/objects-with-imports/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/objects-with-imports/core/schemas/builders/object/object.ts +++ b/seed/ts-express/objects-with-imports/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/objects-with-imports/core/schemas/builders/record/record.ts b/seed/ts-express/objects-with-imports/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/objects-with-imports/core/schemas/builders/record/record.ts +++ b/seed/ts-express/objects-with-imports/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/objects-with-imports/core/schemas/builders/union/union.ts b/seed/ts-express/objects-with-imports/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/objects-with-imports/core/schemas/builders/union/union.ts +++ b/seed/ts-express/objects-with-imports/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/objects-with-imports/core/schemas/utils/isPlainObject.ts b/seed/ts-express/objects-with-imports/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/objects-with-imports/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/objects-with-imports/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/optional/core/schemas/builders/list/list.ts b/seed/ts-express/optional/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/optional/core/schemas/builders/list/list.ts +++ b/seed/ts-express/optional/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/optional/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/optional/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/optional/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/optional/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/optional/core/schemas/builders/object/object.ts b/seed/ts-express/optional/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/optional/core/schemas/builders/object/object.ts +++ b/seed/ts-express/optional/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/optional/core/schemas/builders/record/record.ts b/seed/ts-express/optional/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/optional/core/schemas/builders/record/record.ts +++ b/seed/ts-express/optional/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/optional/core/schemas/builders/union/union.ts b/seed/ts-express/optional/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/optional/core/schemas/builders/union/union.ts +++ b/seed/ts-express/optional/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/optional/core/schemas/utils/isPlainObject.ts b/seed/ts-express/optional/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/optional/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/optional/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/package-yml/core/schemas/builders/list/list.ts b/seed/ts-express/package-yml/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/package-yml/core/schemas/builders/list/list.ts +++ b/seed/ts-express/package-yml/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/package-yml/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/package-yml/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/package-yml/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/package-yml/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/package-yml/core/schemas/builders/object/object.ts b/seed/ts-express/package-yml/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/package-yml/core/schemas/builders/object/object.ts +++ b/seed/ts-express/package-yml/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/package-yml/core/schemas/builders/record/record.ts b/seed/ts-express/package-yml/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/package-yml/core/schemas/builders/record/record.ts +++ b/seed/ts-express/package-yml/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/package-yml/core/schemas/builders/union/union.ts b/seed/ts-express/package-yml/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/package-yml/core/schemas/builders/union/union.ts +++ b/seed/ts-express/package-yml/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/package-yml/core/schemas/utils/isPlainObject.ts b/seed/ts-express/package-yml/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/package-yml/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/package-yml/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/pagination-custom/core/schemas/builders/list/list.ts b/seed/ts-express/pagination-custom/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/pagination-custom/core/schemas/builders/list/list.ts +++ b/seed/ts-express/pagination-custom/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/pagination-custom/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/pagination-custom/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/pagination-custom/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/pagination-custom/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/pagination-custom/core/schemas/builders/object/object.ts b/seed/ts-express/pagination-custom/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/pagination-custom/core/schemas/builders/object/object.ts +++ b/seed/ts-express/pagination-custom/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/pagination-custom/core/schemas/builders/record/record.ts b/seed/ts-express/pagination-custom/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/pagination-custom/core/schemas/builders/record/record.ts +++ b/seed/ts-express/pagination-custom/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/pagination-custom/core/schemas/builders/union/union.ts b/seed/ts-express/pagination-custom/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/pagination-custom/core/schemas/builders/union/union.ts +++ b/seed/ts-express/pagination-custom/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/pagination-custom/core/schemas/utils/isPlainObject.ts b/seed/ts-express/pagination-custom/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/pagination-custom/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/pagination-custom/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/pagination/core/schemas/builders/list/list.ts b/seed/ts-express/pagination/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/pagination/core/schemas/builders/list/list.ts +++ b/seed/ts-express/pagination/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/pagination/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/pagination/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/pagination/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/pagination/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/pagination/core/schemas/builders/object/object.ts b/seed/ts-express/pagination/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/pagination/core/schemas/builders/object/object.ts +++ b/seed/ts-express/pagination/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/pagination/core/schemas/builders/record/record.ts b/seed/ts-express/pagination/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/pagination/core/schemas/builders/record/record.ts +++ b/seed/ts-express/pagination/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/pagination/core/schemas/builders/union/union.ts b/seed/ts-express/pagination/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/pagination/core/schemas/builders/union/union.ts +++ b/seed/ts-express/pagination/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/pagination/core/schemas/utils/isPlainObject.ts b/seed/ts-express/pagination/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/pagination/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/pagination/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/reserved-keywords/core/schemas/builders/list/list.ts b/seed/ts-express/reserved-keywords/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/reserved-keywords/core/schemas/builders/list/list.ts +++ b/seed/ts-express/reserved-keywords/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/reserved-keywords/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/reserved-keywords/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/reserved-keywords/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/reserved-keywords/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/reserved-keywords/core/schemas/builders/object/object.ts b/seed/ts-express/reserved-keywords/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/reserved-keywords/core/schemas/builders/object/object.ts +++ b/seed/ts-express/reserved-keywords/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/reserved-keywords/core/schemas/builders/record/record.ts b/seed/ts-express/reserved-keywords/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/reserved-keywords/core/schemas/builders/record/record.ts +++ b/seed/ts-express/reserved-keywords/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/reserved-keywords/core/schemas/builders/union/union.ts b/seed/ts-express/reserved-keywords/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/reserved-keywords/core/schemas/builders/union/union.ts +++ b/seed/ts-express/reserved-keywords/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/reserved-keywords/core/schemas/utils/isPlainObject.ts b/seed/ts-express/reserved-keywords/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/reserved-keywords/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/reserved-keywords/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/response-property/core/schemas/builders/list/list.ts b/seed/ts-express/response-property/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/response-property/core/schemas/builders/list/list.ts +++ b/seed/ts-express/response-property/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/response-property/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/response-property/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/response-property/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/response-property/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/response-property/core/schemas/builders/object/object.ts b/seed/ts-express/response-property/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/response-property/core/schemas/builders/object/object.ts +++ b/seed/ts-express/response-property/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/response-property/core/schemas/builders/record/record.ts b/seed/ts-express/response-property/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/response-property/core/schemas/builders/record/record.ts +++ b/seed/ts-express/response-property/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/response-property/core/schemas/builders/union/union.ts b/seed/ts-express/response-property/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/response-property/core/schemas/builders/union/union.ts +++ b/seed/ts-express/response-property/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/response-property/core/schemas/utils/isPlainObject.ts b/seed/ts-express/response-property/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/response-property/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/response-property/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/server-url-templating/core/schemas/builders/list/list.ts b/seed/ts-express/server-url-templating/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/server-url-templating/core/schemas/builders/list/list.ts +++ b/seed/ts-express/server-url-templating/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/server-url-templating/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/server-url-templating/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/server-url-templating/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/server-url-templating/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/server-url-templating/core/schemas/builders/object/object.ts b/seed/ts-express/server-url-templating/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/server-url-templating/core/schemas/builders/object/object.ts +++ b/seed/ts-express/server-url-templating/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/server-url-templating/core/schemas/builders/record/record.ts b/seed/ts-express/server-url-templating/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/server-url-templating/core/schemas/builders/record/record.ts +++ b/seed/ts-express/server-url-templating/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/server-url-templating/core/schemas/builders/union/union.ts b/seed/ts-express/server-url-templating/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/server-url-templating/core/schemas/builders/union/union.ts +++ b/seed/ts-express/server-url-templating/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/server-url-templating/core/schemas/utils/isPlainObject.ts b/seed/ts-express/server-url-templating/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/server-url-templating/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/server-url-templating/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/simple-api/core/schemas/builders/list/list.ts b/seed/ts-express/simple-api/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/simple-api/core/schemas/builders/list/list.ts +++ b/seed/ts-express/simple-api/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/simple-api/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/simple-api/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/simple-api/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/simple-api/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/simple-api/core/schemas/builders/object/object.ts b/seed/ts-express/simple-api/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/simple-api/core/schemas/builders/object/object.ts +++ b/seed/ts-express/simple-api/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/simple-api/core/schemas/builders/record/record.ts b/seed/ts-express/simple-api/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/simple-api/core/schemas/builders/record/record.ts +++ b/seed/ts-express/simple-api/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/simple-api/core/schemas/builders/union/union.ts b/seed/ts-express/simple-api/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/simple-api/core/schemas/builders/union/union.ts +++ b/seed/ts-express/simple-api/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/simple-api/core/schemas/utils/isPlainObject.ts b/seed/ts-express/simple-api/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/simple-api/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/simple-api/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/simple-fhir/core/schemas/builders/list/list.ts b/seed/ts-express/simple-fhir/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/simple-fhir/core/schemas/builders/list/list.ts +++ b/seed/ts-express/simple-fhir/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/simple-fhir/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/simple-fhir/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/simple-fhir/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/simple-fhir/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/simple-fhir/core/schemas/builders/object/object.ts b/seed/ts-express/simple-fhir/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/simple-fhir/core/schemas/builders/object/object.ts +++ b/seed/ts-express/simple-fhir/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/simple-fhir/core/schemas/builders/record/record.ts b/seed/ts-express/simple-fhir/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/simple-fhir/core/schemas/builders/record/record.ts +++ b/seed/ts-express/simple-fhir/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/simple-fhir/core/schemas/builders/union/union.ts b/seed/ts-express/simple-fhir/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/simple-fhir/core/schemas/builders/union/union.ts +++ b/seed/ts-express/simple-fhir/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/simple-fhir/core/schemas/utils/isPlainObject.ts b/seed/ts-express/simple-fhir/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/simple-fhir/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/simple-fhir/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/single-url-environment-default/core/schemas/builders/list/list.ts b/seed/ts-express/single-url-environment-default/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/single-url-environment-default/core/schemas/builders/list/list.ts +++ b/seed/ts-express/single-url-environment-default/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/single-url-environment-default/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/single-url-environment-default/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/single-url-environment-default/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/single-url-environment-default/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/single-url-environment-default/core/schemas/builders/object/object.ts b/seed/ts-express/single-url-environment-default/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/single-url-environment-default/core/schemas/builders/object/object.ts +++ b/seed/ts-express/single-url-environment-default/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/single-url-environment-default/core/schemas/builders/record/record.ts b/seed/ts-express/single-url-environment-default/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/single-url-environment-default/core/schemas/builders/record/record.ts +++ b/seed/ts-express/single-url-environment-default/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/single-url-environment-default/core/schemas/builders/union/union.ts b/seed/ts-express/single-url-environment-default/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/single-url-environment-default/core/schemas/builders/union/union.ts +++ b/seed/ts-express/single-url-environment-default/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/single-url-environment-default/core/schemas/utils/isPlainObject.ts b/seed/ts-express/single-url-environment-default/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/single-url-environment-default/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/single-url-environment-default/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/single-url-environment-no-default/core/schemas/builders/list/list.ts b/seed/ts-express/single-url-environment-no-default/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/single-url-environment-no-default/core/schemas/builders/list/list.ts +++ b/seed/ts-express/single-url-environment-no-default/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/single-url-environment-no-default/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/single-url-environment-no-default/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/single-url-environment-no-default/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/single-url-environment-no-default/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/single-url-environment-no-default/core/schemas/builders/object/object.ts b/seed/ts-express/single-url-environment-no-default/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/single-url-environment-no-default/core/schemas/builders/object/object.ts +++ b/seed/ts-express/single-url-environment-no-default/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/single-url-environment-no-default/core/schemas/builders/record/record.ts b/seed/ts-express/single-url-environment-no-default/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/single-url-environment-no-default/core/schemas/builders/record/record.ts +++ b/seed/ts-express/single-url-environment-no-default/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/single-url-environment-no-default/core/schemas/builders/union/union.ts b/seed/ts-express/single-url-environment-no-default/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/single-url-environment-no-default/core/schemas/builders/union/union.ts +++ b/seed/ts-express/single-url-environment-no-default/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/single-url-environment-no-default/core/schemas/utils/isPlainObject.ts b/seed/ts-express/single-url-environment-no-default/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/single-url-environment-no-default/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/single-url-environment-no-default/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/trace/no-custom-config/core/schemas/builders/list/list.ts b/seed/ts-express/trace/no-custom-config/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/trace/no-custom-config/core/schemas/builders/list/list.ts +++ b/seed/ts-express/trace/no-custom-config/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/trace/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/trace/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/trace/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/trace/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/trace/no-custom-config/core/schemas/builders/object/object.ts b/seed/ts-express/trace/no-custom-config/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/trace/no-custom-config/core/schemas/builders/object/object.ts +++ b/seed/ts-express/trace/no-custom-config/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/trace/no-custom-config/core/schemas/builders/record/record.ts b/seed/ts-express/trace/no-custom-config/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/trace/no-custom-config/core/schemas/builders/record/record.ts +++ b/seed/ts-express/trace/no-custom-config/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/trace/no-custom-config/core/schemas/builders/union/union.ts b/seed/ts-express/trace/no-custom-config/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/trace/no-custom-config/core/schemas/builders/union/union.ts +++ b/seed/ts-express/trace/no-custom-config/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/trace/no-custom-config/core/schemas/utils/isPlainObject.ts b/seed/ts-express/trace/no-custom-config/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/trace/no-custom-config/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/trace/no-custom-config/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/list/list.ts b/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/list/list.ts +++ b/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/object/object.ts b/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/object/object.ts +++ b/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/record/record.ts b/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/record/record.ts +++ b/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/union/union.ts b/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/union/union.ts +++ b/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/utils/isPlainObject.ts b/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/ts-extra-properties/core/schemas/builders/list/list.ts b/seed/ts-express/ts-extra-properties/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/ts-extra-properties/core/schemas/builders/list/list.ts +++ b/seed/ts-express/ts-extra-properties/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/ts-extra-properties/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/ts-extra-properties/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/ts-extra-properties/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/ts-extra-properties/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/ts-extra-properties/core/schemas/builders/object/object.ts b/seed/ts-express/ts-extra-properties/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/ts-extra-properties/core/schemas/builders/object/object.ts +++ b/seed/ts-express/ts-extra-properties/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/ts-extra-properties/core/schemas/builders/record/record.ts b/seed/ts-express/ts-extra-properties/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/ts-extra-properties/core/schemas/builders/record/record.ts +++ b/seed/ts-express/ts-extra-properties/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/ts-extra-properties/core/schemas/builders/union/union.ts b/seed/ts-express/ts-extra-properties/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/ts-extra-properties/core/schemas/builders/union/union.ts +++ b/seed/ts-express/ts-extra-properties/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/ts-extra-properties/core/schemas/utils/isPlainObject.ts b/seed/ts-express/ts-extra-properties/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/ts-extra-properties/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/ts-extra-properties/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/ts-inline-types/core/schemas/builders/list/list.ts b/seed/ts-express/ts-inline-types/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/ts-inline-types/core/schemas/builders/list/list.ts +++ b/seed/ts-express/ts-inline-types/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/ts-inline-types/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/ts-inline-types/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/ts-inline-types/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/ts-inline-types/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/ts-inline-types/core/schemas/builders/object/object.ts b/seed/ts-express/ts-inline-types/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/ts-inline-types/core/schemas/builders/object/object.ts +++ b/seed/ts-express/ts-inline-types/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/ts-inline-types/core/schemas/builders/record/record.ts b/seed/ts-express/ts-inline-types/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/ts-inline-types/core/schemas/builders/record/record.ts +++ b/seed/ts-express/ts-inline-types/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/ts-inline-types/core/schemas/builders/union/union.ts b/seed/ts-express/ts-inline-types/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/ts-inline-types/core/schemas/builders/union/union.ts +++ b/seed/ts-express/ts-inline-types/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/ts-inline-types/core/schemas/utils/isPlainObject.ts b/seed/ts-express/ts-inline-types/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/ts-inline-types/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/ts-inline-types/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/list/list.ts b/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/list/list.ts +++ b/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/object/object.ts b/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/object/object.ts +++ b/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/record/record.ts b/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/record/record.ts +++ b/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/union/union.ts b/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/union/union.ts +++ b/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/utils/isPlainObject.ts b/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/undiscriminated-unions/core/schemas/builders/list/list.ts b/seed/ts-express/undiscriminated-unions/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/undiscriminated-unions/core/schemas/builders/list/list.ts +++ b/seed/ts-express/undiscriminated-unions/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/undiscriminated-unions/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/undiscriminated-unions/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/undiscriminated-unions/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/undiscriminated-unions/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/undiscriminated-unions/core/schemas/builders/object/object.ts b/seed/ts-express/undiscriminated-unions/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/undiscriminated-unions/core/schemas/builders/object/object.ts +++ b/seed/ts-express/undiscriminated-unions/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/undiscriminated-unions/core/schemas/builders/record/record.ts b/seed/ts-express/undiscriminated-unions/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/undiscriminated-unions/core/schemas/builders/record/record.ts +++ b/seed/ts-express/undiscriminated-unions/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/undiscriminated-unions/core/schemas/builders/union/union.ts b/seed/ts-express/undiscriminated-unions/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/undiscriminated-unions/core/schemas/builders/union/union.ts +++ b/seed/ts-express/undiscriminated-unions/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/undiscriminated-unions/core/schemas/utils/isPlainObject.ts b/seed/ts-express/undiscriminated-unions/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/undiscriminated-unions/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/undiscriminated-unions/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/unions-with-local-date/core/schemas/builders/list/list.ts b/seed/ts-express/unions-with-local-date/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/unions-with-local-date/core/schemas/builders/list/list.ts +++ b/seed/ts-express/unions-with-local-date/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/unions-with-local-date/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/unions-with-local-date/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/unions-with-local-date/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/unions-with-local-date/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/unions-with-local-date/core/schemas/builders/object/object.ts b/seed/ts-express/unions-with-local-date/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/unions-with-local-date/core/schemas/builders/object/object.ts +++ b/seed/ts-express/unions-with-local-date/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/unions-with-local-date/core/schemas/builders/record/record.ts b/seed/ts-express/unions-with-local-date/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/unions-with-local-date/core/schemas/builders/record/record.ts +++ b/seed/ts-express/unions-with-local-date/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/unions-with-local-date/core/schemas/builders/union/union.ts b/seed/ts-express/unions-with-local-date/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/unions-with-local-date/core/schemas/builders/union/union.ts +++ b/seed/ts-express/unions-with-local-date/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/unions-with-local-date/core/schemas/utils/isPlainObject.ts b/seed/ts-express/unions-with-local-date/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/unions-with-local-date/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/unions-with-local-date/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/unions/core/schemas/builders/list/list.ts b/seed/ts-express/unions/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/unions/core/schemas/builders/list/list.ts +++ b/seed/ts-express/unions/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/unions/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/unions/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/unions/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/unions/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/unions/core/schemas/builders/object/object.ts b/seed/ts-express/unions/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/unions/core/schemas/builders/object/object.ts +++ b/seed/ts-express/unions/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/unions/core/schemas/builders/record/record.ts b/seed/ts-express/unions/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/unions/core/schemas/builders/record/record.ts +++ b/seed/ts-express/unions/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/unions/core/schemas/builders/union/union.ts b/seed/ts-express/unions/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/unions/core/schemas/builders/union/union.ts +++ b/seed/ts-express/unions/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/unions/core/schemas/utils/isPlainObject.ts b/seed/ts-express/unions/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/unions/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/unions/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/unknown/core/schemas/builders/list/list.ts b/seed/ts-express/unknown/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/unknown/core/schemas/builders/list/list.ts +++ b/seed/ts-express/unknown/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/unknown/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/unknown/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/unknown/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/unknown/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/unknown/core/schemas/builders/object/object.ts b/seed/ts-express/unknown/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/unknown/core/schemas/builders/object/object.ts +++ b/seed/ts-express/unknown/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/unknown/core/schemas/builders/record/record.ts b/seed/ts-express/unknown/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/unknown/core/schemas/builders/record/record.ts +++ b/seed/ts-express/unknown/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/unknown/core/schemas/builders/union/union.ts b/seed/ts-express/unknown/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/unknown/core/schemas/builders/union/union.ts +++ b/seed/ts-express/unknown/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/unknown/core/schemas/utils/isPlainObject.ts b/seed/ts-express/unknown/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/unknown/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/unknown/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/url-form-encoded/core/schemas/builders/list/list.ts b/seed/ts-express/url-form-encoded/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/url-form-encoded/core/schemas/builders/list/list.ts +++ b/seed/ts-express/url-form-encoded/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/url-form-encoded/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/url-form-encoded/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/url-form-encoded/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/url-form-encoded/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/url-form-encoded/core/schemas/builders/object/object.ts b/seed/ts-express/url-form-encoded/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/url-form-encoded/core/schemas/builders/object/object.ts +++ b/seed/ts-express/url-form-encoded/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/url-form-encoded/core/schemas/builders/record/record.ts b/seed/ts-express/url-form-encoded/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/url-form-encoded/core/schemas/builders/record/record.ts +++ b/seed/ts-express/url-form-encoded/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/url-form-encoded/core/schemas/builders/union/union.ts b/seed/ts-express/url-form-encoded/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/url-form-encoded/core/schemas/builders/union/union.ts +++ b/seed/ts-express/url-form-encoded/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/url-form-encoded/core/schemas/utils/isPlainObject.ts b/seed/ts-express/url-form-encoded/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/url-form-encoded/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/url-form-encoded/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/validation/core/schemas/builders/list/list.ts b/seed/ts-express/validation/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/validation/core/schemas/builders/list/list.ts +++ b/seed/ts-express/validation/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/validation/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/validation/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/validation/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/validation/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/validation/core/schemas/builders/object/object.ts b/seed/ts-express/validation/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/validation/core/schemas/builders/object/object.ts +++ b/seed/ts-express/validation/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/validation/core/schemas/builders/record/record.ts b/seed/ts-express/validation/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/validation/core/schemas/builders/record/record.ts +++ b/seed/ts-express/validation/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/validation/core/schemas/builders/union/union.ts b/seed/ts-express/validation/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/validation/core/schemas/builders/union/union.ts +++ b/seed/ts-express/validation/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/validation/core/schemas/utils/isPlainObject.ts b/seed/ts-express/validation/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/validation/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/validation/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/version-no-default/core/schemas/builders/list/list.ts b/seed/ts-express/version-no-default/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/version-no-default/core/schemas/builders/list/list.ts +++ b/seed/ts-express/version-no-default/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/version-no-default/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/version-no-default/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/version-no-default/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/version-no-default/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/version-no-default/core/schemas/builders/object/object.ts b/seed/ts-express/version-no-default/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/version-no-default/core/schemas/builders/object/object.ts +++ b/seed/ts-express/version-no-default/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/version-no-default/core/schemas/builders/record/record.ts b/seed/ts-express/version-no-default/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/version-no-default/core/schemas/builders/record/record.ts +++ b/seed/ts-express/version-no-default/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/version-no-default/core/schemas/builders/union/union.ts b/seed/ts-express/version-no-default/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/version-no-default/core/schemas/builders/union/union.ts +++ b/seed/ts-express/version-no-default/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/version-no-default/core/schemas/utils/isPlainObject.ts b/seed/ts-express/version-no-default/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/version-no-default/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/version-no-default/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/version/core/schemas/builders/list/list.ts b/seed/ts-express/version/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/version/core/schemas/builders/list/list.ts +++ b/seed/ts-express/version/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/version/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/version/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/version/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/version/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/version/core/schemas/builders/object/object.ts b/seed/ts-express/version/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/version/core/schemas/builders/object/object.ts +++ b/seed/ts-express/version/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/version/core/schemas/builders/record/record.ts b/seed/ts-express/version/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/version/core/schemas/builders/record/record.ts +++ b/seed/ts-express/version/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/version/core/schemas/builders/union/union.ts b/seed/ts-express/version/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/version/core/schemas/builders/union/union.ts +++ b/seed/ts-express/version/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/version/core/schemas/utils/isPlainObject.ts b/seed/ts-express/version/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/version/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/version/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/webhook-audience/core/schemas/builders/list/list.ts b/seed/ts-express/webhook-audience/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/webhook-audience/core/schemas/builders/list/list.ts +++ b/seed/ts-express/webhook-audience/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/webhook-audience/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/webhook-audience/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/webhook-audience/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/webhook-audience/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/webhook-audience/core/schemas/builders/object/object.ts b/seed/ts-express/webhook-audience/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/webhook-audience/core/schemas/builders/object/object.ts +++ b/seed/ts-express/webhook-audience/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/webhook-audience/core/schemas/builders/record/record.ts b/seed/ts-express/webhook-audience/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/webhook-audience/core/schemas/builders/record/record.ts +++ b/seed/ts-express/webhook-audience/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/webhook-audience/core/schemas/builders/union/union.ts b/seed/ts-express/webhook-audience/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/webhook-audience/core/schemas/builders/union/union.ts +++ b/seed/ts-express/webhook-audience/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/webhook-audience/core/schemas/utils/isPlainObject.ts b/seed/ts-express/webhook-audience/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/webhook-audience/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/webhook-audience/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/websocket-bearer-auth/core/schemas/builders/list/list.ts b/seed/ts-express/websocket-bearer-auth/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/websocket-bearer-auth/core/schemas/builders/list/list.ts +++ b/seed/ts-express/websocket-bearer-auth/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/websocket-bearer-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/websocket-bearer-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/websocket-bearer-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/websocket-bearer-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/websocket-bearer-auth/core/schemas/builders/object/object.ts b/seed/ts-express/websocket-bearer-auth/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/websocket-bearer-auth/core/schemas/builders/object/object.ts +++ b/seed/ts-express/websocket-bearer-auth/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/websocket-bearer-auth/core/schemas/builders/record/record.ts b/seed/ts-express/websocket-bearer-auth/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/websocket-bearer-auth/core/schemas/builders/record/record.ts +++ b/seed/ts-express/websocket-bearer-auth/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/websocket-bearer-auth/core/schemas/builders/union/union.ts b/seed/ts-express/websocket-bearer-auth/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/websocket-bearer-auth/core/schemas/builders/union/union.ts +++ b/seed/ts-express/websocket-bearer-auth/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/websocket-bearer-auth/core/schemas/utils/isPlainObject.ts b/seed/ts-express/websocket-bearer-auth/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/websocket-bearer-auth/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/websocket-bearer-auth/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/websocket-inferred-auth/core/schemas/builders/list/list.ts b/seed/ts-express/websocket-inferred-auth/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/websocket-inferred-auth/core/schemas/builders/list/list.ts +++ b/seed/ts-express/websocket-inferred-auth/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/websocket-inferred-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/websocket-inferred-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/websocket-inferred-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/websocket-inferred-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/websocket-inferred-auth/core/schemas/builders/object/object.ts b/seed/ts-express/websocket-inferred-auth/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/websocket-inferred-auth/core/schemas/builders/object/object.ts +++ b/seed/ts-express/websocket-inferred-auth/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/websocket-inferred-auth/core/schemas/builders/record/record.ts b/seed/ts-express/websocket-inferred-auth/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/websocket-inferred-auth/core/schemas/builders/record/record.ts +++ b/seed/ts-express/websocket-inferred-auth/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/websocket-inferred-auth/core/schemas/builders/union/union.ts b/seed/ts-express/websocket-inferred-auth/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/websocket-inferred-auth/core/schemas/builders/union/union.ts +++ b/seed/ts-express/websocket-inferred-auth/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/websocket-inferred-auth/core/schemas/utils/isPlainObject.ts b/seed/ts-express/websocket-inferred-auth/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/websocket-inferred-auth/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/websocket-inferred-auth/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/websocket/core/schemas/builders/list/list.ts b/seed/ts-express/websocket/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/websocket/core/schemas/builders/list/list.ts +++ b/seed/ts-express/websocket/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/websocket/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/websocket/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/websocket/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/websocket/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/websocket/core/schemas/builders/object/object.ts b/seed/ts-express/websocket/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/websocket/core/schemas/builders/object/object.ts +++ b/seed/ts-express/websocket/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/websocket/core/schemas/builders/record/record.ts b/seed/ts-express/websocket/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/websocket/core/schemas/builders/record/record.ts +++ b/seed/ts-express/websocket/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/websocket/core/schemas/builders/union/union.ts b/seed/ts-express/websocket/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/websocket/core/schemas/builders/union/union.ts +++ b/seed/ts-express/websocket/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/websocket/core/schemas/utils/isPlainObject.ts b/seed/ts-express/websocket/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/websocket/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/websocket/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } From 97fe949a434030f15e3cd96edfd3fed0c21e84b3 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Mon, 23 Feb 2026 21:08:54 -0500 Subject: [PATCH 04/10] feat(cli): auto-correct fern-api/ to fernapi/ in generator names (#12684) Co-authored-by: unknown <> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- packages/cli/cli/src/cli.ts | 25 +++++- packages/cli/cli/src/cliV2.ts | 33 ++++++-- packages/cli/cli/versions.yml | 10 +++ .../__test__/getGeneratorName.test.ts | 81 +++++++++++++++++++ .../convertGeneratorsConfiguration.ts | 20 +++-- .../src/generators-yml/getGeneratorName.ts | 37 +++++++++ .../src/generators-yml/index.ts | 4 + 7 files changed, 196 insertions(+), 14 deletions(-) diff --git a/packages/cli/cli/src/cli.ts b/packages/cli/cli/src/cli.ts index eaed800b7f7f..27a7814f6e71 100644 --- a/packages/cli/cli/src/cli.ts +++ b/packages/cli/cli/src/cli.ts @@ -4,9 +4,11 @@ import { fromBinary, toBinary } from "@bufbuild/protobuf"; import { CodeGeneratorRequestSchema, CodeGeneratorResponseSchema } from "@bufbuild/protobuf/wkt"; import { runCliV2 } from "@fern-api/cli-v2"; import { + correctIncorrectDockerOrg, GENERATORS_CONFIGURATION_FILENAME, generatorsYml, getFernDirectory, + INCORRECT_DOCKER_ORG, loadProjectConfig, PROJECT_CONFIG_FILENAME } from "@fern-api/configuration-loader"; @@ -550,12 +552,13 @@ function addAddCommand(cli: Argv, cliContext: CliContext) { description: "Add the generator to the specified group" }), async (argv) => { + const generatorName = warnAndCorrectIncorrectDockerOrg(argv.generator, cliContext); await addGeneratorToWorkspaces({ project: await loadProjectAndRegisterWorkspacesWithContext(cliContext, { commandLineApiWorkspace: argv.api, defaultToAllApiWorkspaces: false }), - generatorName: argv.generator, + generatorName, groupName: argv.group, cliContext }); @@ -714,6 +717,8 @@ function addGenerateCommand(cli: Argv, cliContext: CliContext) if (argv.output != null && argv.docs != null) { return cliContext.failWithoutThrowing("The --output flag is not supported for docs generation."); } + const correctedGeneratorFilter = + argv.generator != null ? warnAndCorrectIncorrectDockerOrg(argv.generator, cliContext) : undefined; if (argv.api != null) { return await generateAPIWorkspaces({ project: await loadProjectAndRegisterWorkspacesWithContext(cliContext, { @@ -723,7 +728,7 @@ function addGenerateCommand(cli: Argv, cliContext: CliContext) cliContext, version: argv.version, groupName: argv.group, - generatorName: argv.generator, + generatorName: correctedGeneratorFilter, shouldLogS3Url: argv.printZipUrl, keepDocker: argv.keepDocker, useLocalDocker: argv.local || argv.runner != null, @@ -776,7 +781,7 @@ function addGenerateCommand(cli: Argv, cliContext: CliContext) cliContext, version: argv.version, groupName: argv.group, - generatorName: argv.generator, + generatorName: correctedGeneratorFilter, shouldLogS3Url: argv.printZipUrl, keepDocker: argv.keepDocker, useLocalDocker: argv.local, @@ -1993,6 +1998,20 @@ function readBytes(stream: ReadStream): Promise { }); } +/** + * Corrects the incorrect "fern-api/" Docker org prefix to "fernapi/" and logs a warning. + * Used for CLI arguments that accept generator names. + */ +function warnAndCorrectIncorrectDockerOrg(generatorName: string, cliContext: CliContext): string { + const corrected = correctIncorrectDockerOrg(generatorName); + if (corrected !== generatorName) { + cliContext.logger.warn( + `"${generatorName}" is not a valid generator name. Using "${corrected}" instead — the Docker org is "fernapi", not "${INCORRECT_DOCKER_ORG}".` + ); + } + return corrected; +} + function writeBytes(stream: WriteStream, data: Uint8Array): Promise { return new Promise((resolve, reject) => { stream.write(data, (err) => { diff --git a/packages/cli/cli/src/cliV2.ts b/packages/cli/cli/src/cliV2.ts index 2d9f74067cad..e562368c6c68 100644 --- a/packages/cli/cli/src/cliV2.ts +++ b/packages/cli/cli/src/cliV2.ts @@ -1,4 +1,8 @@ -import { GENERATORS_CONFIGURATION_FILENAME } from "@fern-api/configuration-loader"; +import { + correctIncorrectDockerOrg, + GENERATORS_CONFIGURATION_FILENAME, + INCORRECT_DOCKER_ORG +} from "@fern-api/configuration-loader"; import { FernRegistry } from "@fern-fern/generators-sdk"; import { writeFile } from "fs/promises"; import { Argv } from "yargs"; @@ -12,6 +16,20 @@ import { getGeneratorMetadata } from "./commands/generator-metadata/getGenerator import { getOrganization } from "./commands/organization/getOrganization.js"; import { upgradeGenerator } from "./commands/upgrade/upgradeGenerator.js"; +/** + * Corrects the incorrect "fern-api/" Docker org prefix to "fernapi/" and logs a warning. + * Used for CLI arguments that accept generator names. + */ +function warnAndCorrectIncorrectDockerOrgV2(generatorName: string, cliContext: CliContext): string { + const corrected = correctIncorrectDockerOrg(generatorName); + if (corrected !== generatorName) { + cliContext.logger.warn( + `"${generatorName}" is not a valid generator name. Using "${corrected}" instead — the Docker org is "fernapi", not "${INCORRECT_DOCKER_ORG}".` + ); + } + return corrected; +} + export function addGetOrganizationCommand(cli: Argv, cliContext: CliContext): void { cli.command( "organization", @@ -177,6 +195,10 @@ export function addGeneratorCommands(cli: Argv, cliContext: Cl } }); + const correctedGenerator = + argv.generator != null + ? warnAndCorrectIncorrectDockerOrgV2(argv.generator, cliContext) + : undefined; const project = await loadProjectAndRegisterWorkspacesWithContext(cliContext, { commandLineApiWorkspace: argv.api, defaultToAllApiWorkspaces: true @@ -188,7 +210,7 @@ export function addGeneratorCommands(cli: Argv, cliContext: Cl const upgrades = await getProjectGeneratorUpgrades({ cliContext, project, - generatorFilter: argv.generator, + generatorFilter: correctedGenerator, groupFilter: argv.group, includeMajor: argv.includeMajor, channel: argv.channel @@ -205,7 +227,7 @@ export function addGeneratorCommands(cli: Argv, cliContext: Cl } else { await upgradeGenerator({ cliContext, - generator: argv.generator, + generator: correctedGenerator, group: argv.group, project: await loadProjectAndRegisterWorkspacesWithContext(cliContext, { commandLineApiWorkspace: argv.api, @@ -259,10 +281,11 @@ export function addGeneratorCommands(cli: Argv, cliContext: Cl description: "Get repository for the generator invocation, if one is specified." }), async (argv) => { + const correctedGetGenerator = warnAndCorrectIncorrectDockerOrgV2(argv.generator, cliContext); await cliContext.instrumentPostHogEvent({ command: "fern generator get", properties: { - generator: argv.generator, + generator: correctedGetGenerator, version: argv.version, api: argv.api, group: argv.group, @@ -271,7 +294,7 @@ export function addGeneratorCommands(cli: Argv, cliContext: Cl }); const generator = await getGeneratorMetadata({ cliContext, - generatorFilter: argv.generator, + generatorFilter: correctedGetGenerator, groupFilter: argv.group, apiFilter: argv.api, project: await loadProjectAndRegisterWorkspacesWithContext(cliContext, { diff --git a/packages/cli/cli/versions.yml b/packages/cli/cli/versions.yml index dcf3a9dd67fe..b5f5986613bc 100644 --- a/packages/cli/cli/versions.yml +++ b/packages/cli/cli/versions.yml @@ -1,4 +1,14 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 3.85.0 + changelogEntry: + - summary: | + Auto-correct incorrect `fern-api/` Docker org prefix to `fernapi/` in generator names. + When a generator is specified with `fern-api/` (the GitHub/npm org) instead of `fernapi/` + (the Docker Hub org), the CLI now automatically corrects it and prints a warning. + This applies to CLI commands (`--generator` flag) and `generators.yml` configuration files. + type: feat + createdAt: "2026-02-24" + irVersion: 65 - version: 3.84.0 changelogEntry: - summary: | diff --git a/packages/cli/configuration-loader/src/generators-yml/__test__/getGeneratorName.test.ts b/packages/cli/configuration-loader/src/generators-yml/__test__/getGeneratorName.test.ts index b0ed96532fd7..6fc245effaf8 100644 --- a/packages/cli/configuration-loader/src/generators-yml/__test__/getGeneratorName.test.ts +++ b/packages/cli/configuration-loader/src/generators-yml/__test__/getGeneratorName.test.ts @@ -3,8 +3,11 @@ import { createMockTaskContext } from "@fern-api/task-context"; import { GeneratorName } from "../GeneratorName.js"; import { addDefaultDockerOrgIfNotPresent, + correctIncorrectDockerOrg, + correctIncorrectDockerOrgWithWarning, DEFAULT_DOCKER_ORG, getGeneratorNameOrThrow, + INCORRECT_DOCKER_ORG, normalizeGeneratorName, removeDefaultDockerOrgIfPresent } from "../getGeneratorName.js"; @@ -25,6 +28,11 @@ describe("addDefaultDockerOrgIfNotPresent", () => { expect(addDefaultDockerOrgIfNotPresent("fernapi/fern-typescript-sdk")).toBe("fernapi/fern-typescript-sdk"); }); + it("corrects deprecated fern-api/ prefix to fernapi/", () => { + expect(addDefaultDockerOrgIfNotPresent("fern-api/fern-typescript-sdk")).toBe("fernapi/fern-typescript-sdk"); + expect(addDefaultDockerOrgIfNotPresent("fern-api/fern-python-sdk")).toBe("fernapi/fern-python-sdk"); + }); + it("preserves custom org prefixes", () => { expect(addDefaultDockerOrgIfNotPresent("myorg/my-generator")).toBe("myorg/my-generator"); expect(addDefaultDockerOrgIfNotPresent("customorg/fern-typescript-sdk")).toBe("customorg/fern-typescript-sdk"); @@ -239,4 +247,77 @@ describe("real-world scenarios", () => { expect(fdrApiName).toBe("fernapi/fern-csharp-sdk"); expect(fdrApiName).toContain("/"); }); + + it("handles deprecated fern-api/ org in generators.yml", () => { + // User accidentally uses fern-api/ (GitHub org) instead of fernapi/ (Docker org) + const yamlName = "fern-api/fern-typescript-sdk"; + const normalized = addDefaultDockerOrgIfNotPresent(yamlName); + expect(normalized).toBe("fernapi/fern-typescript-sdk"); + }); + + it("handles deprecated fern-api/ org in CLI --generator filter", () => { + // User passes fern-api/fern-typescript-sdk on CLI + const cliArg = "fern-api/fern-typescript-sdk"; + const normalized = addDefaultDockerOrgIfNotPresent(cliArg); + const yamlNormalized = addDefaultDockerOrgIfNotPresent("fern-typescript-sdk"); + expect(normalized).toBe(yamlNormalized); + }); +}); + +describe("correctIncorrectDockerOrg", () => { + it("corrects fern-api/ to fernapi/", () => { + expect(correctIncorrectDockerOrg("fern-api/fern-typescript-sdk")).toBe("fernapi/fern-typescript-sdk"); + expect(correctIncorrectDockerOrg("fern-api/fern-python-sdk")).toBe("fernapi/fern-python-sdk"); + expect(correctIncorrectDockerOrg("fern-api/fern-java-sdk")).toBe("fernapi/fern-java-sdk"); + }); + + it("does not modify fernapi/ names", () => { + expect(correctIncorrectDockerOrg("fernapi/fern-typescript-sdk")).toBe("fernapi/fern-typescript-sdk"); + }); + + it("does not modify shorthand names", () => { + expect(correctIncorrectDockerOrg("fern-typescript-sdk")).toBe("fern-typescript-sdk"); + }); + + it("does not modify custom org names", () => { + expect(correctIncorrectDockerOrg("myorg/my-generator")).toBe("myorg/my-generator"); + }); + + it("handles empty string", () => { + expect(correctIncorrectDockerOrg("")).toBe(""); + }); +}); + +describe("correctIncorrectDockerOrgWithWarning", () => { + it("corrects fern-api/ to fernapi/ and logs warning", () => { + const context = createMockTaskContext(); + const result = correctIncorrectDockerOrgWithWarning("fern-api/fern-typescript-sdk", context); + expect(result).toBe("fernapi/fern-typescript-sdk"); + }); + + it("does not log warning for correct names", () => { + const context = createMockTaskContext(); + const result = correctIncorrectDockerOrgWithWarning("fernapi/fern-typescript-sdk", context); + expect(result).toBe("fernapi/fern-typescript-sdk"); + }); + + it("does not log warning for shorthand names", () => { + const context = createMockTaskContext(); + const result = correctIncorrectDockerOrgWithWarning("fern-typescript-sdk", context); + expect(result).toBe("fern-typescript-sdk"); + }); +}); + +describe("INCORRECT_DOCKER_ORG constant", () => { + it("is set to 'fern-api'", () => { + expect(INCORRECT_DOCKER_ORG).toBe("fern-api"); + }); +}); + +describe("normalizeGeneratorName with deprecated org", () => { + it("normalizes fern-api/ prefixed names to valid GeneratorName", () => { + expect(normalizeGeneratorName("fern-api/fern-typescript-sdk")).toBe(GeneratorName.TYPESCRIPT_SDK); + expect(normalizeGeneratorName("fern-api/fern-python-sdk")).toBe(GeneratorName.PYTHON_SDK); + expect(normalizeGeneratorName("fern-api/fern-java-sdk")).toBe(GeneratorName.JAVA_SDK); + }); }); diff --git a/packages/cli/configuration-loader/src/generators-yml/convertGeneratorsConfiguration.ts b/packages/cli/configuration-loader/src/generators-yml/convertGeneratorsConfiguration.ts index c88529532eb8..6588849b9e40 100644 --- a/packages/cli/configuration-loader/src/generators-yml/convertGeneratorsConfiguration.ts +++ b/packages/cli/configuration-loader/src/generators-yml/convertGeneratorsConfiguration.ts @@ -8,7 +8,7 @@ import { GithubPullRequestReviewer, OutputMetadata, PublishingMetadata, PypiMeta import { readFile } from "fs/promises"; import path from "path"; -import { addDefaultDockerOrgIfNotPresent } from "./getGeneratorName.js"; +import { addDefaultDockerOrgIfNotPresent, correctIncorrectDockerOrgWithWarning } from "./getGeneratorName.js"; /** * Union type representing any spec-level settings schema. @@ -82,7 +82,8 @@ export async function convertGeneratorsConfiguration({ group, maybeTopLevelMetadata, maybeTopLevelReviewers: rawGeneratorsConfiguration.reviewers, - readme + readme, + context }) ) ) @@ -546,7 +547,8 @@ async function convertGroup({ group, maybeTopLevelMetadata, maybeTopLevelReviewers, - readme + readme, + context }: { absolutePathToGeneratorsConfiguration: AbsoluteFilePath; groupName: string; @@ -554,6 +556,7 @@ async function convertGroup({ maybeTopLevelMetadata: OutputMetadata | undefined; maybeTopLevelReviewers: generatorsYml.ReviewersSchema | undefined; readme: generatorsYml.ReadmeSchema | undefined; + context: TaskContext; }): Promise { const maybeGroupLevelMetadata = getOutputMetadata(group.metadata); return { @@ -569,7 +572,8 @@ async function convertGroup({ maybeGroupLevelMetadata, maybeTopLevelReviewers, maybeGroupLevelReviewers: group.reviewers, - readme + readme, + context }) ) ) @@ -583,7 +587,8 @@ async function convertGenerator({ maybeTopLevelMetadata, maybeGroupLevelReviewers, maybeTopLevelReviewers, - readme + readme, + context }: { absolutePathToGeneratorsConfiguration: AbsoluteFilePath; generator: generatorsYml.GeneratorInvocationSchema; @@ -592,9 +597,12 @@ async function convertGenerator({ maybeGroupLevelReviewers: generatorsYml.ReviewersSchema | undefined; maybeTopLevelReviewers: generatorsYml.ReviewersSchema | undefined; readme: generatorsYml.ReadmeSchema | undefined; + context: TaskContext; }): Promise { + // Warn and correct incorrect "fern-api/" org prefix in generators.yml + const correctedName = correctIncorrectDockerOrgWithWarning(generator.name, context); // Normalize the generator name by adding the default Docker org prefix if not present - const normalizedName = addDefaultDockerOrgIfNotPresent(generator.name); + const normalizedName = addDefaultDockerOrgIfNotPresent(correctedName); return { raw: generator, name: normalizedName, diff --git a/packages/cli/configuration-loader/src/generators-yml/getGeneratorName.ts b/packages/cli/configuration-loader/src/generators-yml/getGeneratorName.ts index 3d2e1372b425..9664105de38a 100644 --- a/packages/cli/configuration-loader/src/generators-yml/getGeneratorName.ts +++ b/packages/cli/configuration-loader/src/generators-yml/getGeneratorName.ts @@ -3,6 +3,7 @@ import { TaskContext } from "@fern-api/task-context"; import { GeneratorName } from "./GeneratorName.js"; export const DEFAULT_DOCKER_ORG = "fernapi"; +export const INCORRECT_DOCKER_ORG = "fern-api"; /** * Adds the default Docker org prefix (fernapi/) to a generator name if no org is specified. @@ -15,12 +16,48 @@ export const DEFAULT_DOCKER_ORG = "fernapi"; * - "myorg/my-generator" -> "myorg/my-generator" (unchanged) */ export function addDefaultDockerOrgIfNotPresent(generatorName: string): string { + // Correct incorrect "fern-api/" org to "fernapi/" silently + generatorName = correctIncorrectDockerOrg(generatorName); if (generatorName.includes("/")) { return generatorName; } return `${DEFAULT_DOCKER_ORG}/${generatorName}`; } +/** + * Corrects the incorrect Docker org prefix "fern-api/" to the correct "fernapi/". + * This handles a common user mistake where "fern-api" (the GitHub/npm org) is + * confused with "fernapi" (the Docker Hub org for Fern generators). + * + * Examples: + * - "fern-api/fern-typescript-sdk" -> "fernapi/fern-typescript-sdk" + * - "fernapi/fern-typescript-sdk" -> "fernapi/fern-typescript-sdk" (unchanged) + * - "myorg/my-generator" -> "myorg/my-generator" (unchanged) + */ +export function correctIncorrectDockerOrg(generatorName: string): string { + const incorrectPrefix = `${INCORRECT_DOCKER_ORG}/`; + if (generatorName.startsWith(incorrectPrefix)) { + return `${DEFAULT_DOCKER_ORG}/${generatorName.slice(incorrectPrefix.length)}`; + } + return generatorName; +} + +/** + * Corrects the incorrect Docker org prefix "fern-api/" to "fernapi/" and logs a warning. + * Use this variant when you have access to a TaskContext for user-visible warnings. + */ +export function correctIncorrectDockerOrgWithWarning(generatorName: string, context: TaskContext): string { + const incorrectPrefix = `${INCORRECT_DOCKER_ORG}/`; + if (generatorName.startsWith(incorrectPrefix)) { + const corrected = `${DEFAULT_DOCKER_ORG}/${generatorName.slice(incorrectPrefix.length)}`; + context.logger.warn( + `"${generatorName}" is not a valid generator name. Using "${corrected}" instead — the Docker org is "fernapi", not "fern-api".` + ); + return corrected; + } + return generatorName; +} + /** * Removes the default Docker org prefix (fernapi/) from a generator name if present. * If the name has a different org prefix, it is returned as-is. diff --git a/packages/cli/configuration-loader/src/generators-yml/index.ts b/packages/cli/configuration-loader/src/generators-yml/index.ts index daf10744a471..d305c87d14a4 100644 --- a/packages/cli/configuration-loader/src/generators-yml/index.ts +++ b/packages/cli/configuration-loader/src/generators-yml/index.ts @@ -8,7 +8,11 @@ export { GeneratorName } from "./GeneratorName.js"; export { GENERATOR_INVOCATIONS } from "./generatorInvocations.js"; export { addDefaultDockerOrgIfNotPresent, + correctIncorrectDockerOrg, + correctIncorrectDockerOrgWithWarning, + DEFAULT_DOCKER_ORG, getGeneratorNameOrThrow, + INCORRECT_DOCKER_ORG, normalizeGeneratorName, removeDefaultDockerOrgIfPresent } from "./getGeneratorName.js"; From 8591d8df2cd3c4e3a2875007d772b400b78192c2 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Mon, 23 Feb 2026 21:16:06 -0500 Subject: [PATCH 05/10] fix(typescript): change [TIMING] logs from info to debug level (#12687) Co-authored-by: unknown <> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- generators/base/src/AbstractGeneratorCli.ts | 8 ++++---- .../csharp/base/src/project/CsharpProject.ts | 14 +++++++------- generators/csharp/model/src/ModelGeneratorCli.ts | 4 ++-- generators/csharp/model/versions.yml | 9 +++++++++ generators/csharp/sdk/src/SdkGeneratorCli.ts | 6 +++--- generators/csharp/sdk/versions.yml | 9 +++++++++ generators/typescript/sdk/versions.yml | 9 +++++++++ .../src/AbstractGeneratorCli.ts | 2 +- .../PersistedTypescriptProject.ts | 14 +++++++------- 9 files changed, 51 insertions(+), 24 deletions(-) diff --git a/generators/base/src/AbstractGeneratorCli.ts b/generators/base/src/AbstractGeneratorCli.ts index cccf3c43dc57..3fa29d418bec 100644 --- a/generators/base/src/AbstractGeneratorCli.ts +++ b/generators/base/src/AbstractGeneratorCli.ts @@ -47,11 +47,11 @@ export abstract class AbstractGeneratorCli< generatorConfig: config, generatorNotificationService }); - context.logger.info(`[TIMING] parseIntermediateRepresentation took ${irElapsed}ms`); - context.logger.info(`[TIMING] constructContext took ${Date.now() - contextStartTime}ms`); + context.logger.debug(`[TIMING] parseIntermediateRepresentation took ${irElapsed}ms`); + context.logger.debug(`[TIMING] constructContext took ${Date.now() - contextStartTime}ms`); const metadataStartTime = Date.now(); await this.generateMetadata(context); - context.logger.info(`[TIMING] generateMetadata took ${Date.now() - metadataStartTime}ms`); + context.logger.debug(`[TIMING] generateMetadata took ${Date.now() - metadataStartTime}ms`); const outputStartTime = Date.now(); switch (config.output.mode.type) { case "publish": @@ -66,7 +66,7 @@ export abstract class AbstractGeneratorCli< default: assertNever(config.output.mode); } - context.logger.info(`[TIMING] output (${config.output.mode.type}) took ${Date.now() - outputStartTime}ms`); + context.logger.debug(`[TIMING] output (${config.output.mode.type}) took ${Date.now() - outputStartTime}ms`); await generatorNotificationService.sendUpdate( FernGeneratorExec.GeneratorUpdate.exitStatusUpdate(FernGeneratorExec.ExitStatusUpdate.successful({})) ); diff --git a/generators/csharp/base/src/project/CsharpProject.ts b/generators/csharp/base/src/project/CsharpProject.ts index c924b52455bd..72cc0409953c 100644 --- a/generators/csharp/base/src/project/CsharpProject.ts +++ b/generators/csharp/base/src/project/CsharpProject.ts @@ -215,26 +215,26 @@ export class CsharpProject extends AbstractProject { libraryPath, solutionPath }); - this.context.logger.info(`[TIMING] createProject took ${Date.now() - createProjectStartTime}ms`); + this.context.logger.debug(`[TIMING] createProject took ${Date.now() - createProjectStartTime}ms`); const createTestProjectStartTime = Date.now(); const absolutePathToTestProjectDirectory = await this.createTestProject({ absolutePathToTestDirectory, absolutePathToSolutionDirectory, absolutePathToProjectDirectory }); - this.context.logger.info(`[TIMING] createTestProject took ${Date.now() - createTestProjectStartTime}ms`); + this.context.logger.debug(`[TIMING] createTestProject took ${Date.now() - createTestProjectStartTime}ms`); const writeSourceFilesStartTime = Date.now(); for (const file of this.sourceFiles) { await file.write(absolutePathToProjectDirectory); } - this.context.logger.info(`[TIMING] writeSourceFiles took ${Date.now() - writeSourceFilesStartTime}ms`); + this.context.logger.debug(`[TIMING] writeSourceFiles took ${Date.now() - writeSourceFilesStartTime}ms`); const writeTestFilesStartTime = Date.now(); for (const file of this.testFiles) { await file.write(absolutePathToTestProjectDirectory); } - this.context.logger.info(`[TIMING] writeTestFiles took ${Date.now() - writeTestFilesStartTime}ms`); + this.context.logger.debug(`[TIMING] writeTestFiles took ${Date.now() - writeTestFilesStartTime}ms`); await this.createRawFiles(); @@ -335,13 +335,13 @@ dotnet_diagnostic.IDE0005.severity = error ` ); } - this.context.logger.info(`[TIMING] dotnetFormat took ${Date.now() - dotnetFormatStartTime}ms`); + this.context.logger.debug(`[TIMING] dotnetFormat took ${Date.now() - dotnetFormatStartTime}ms`); // format the code cleanly using csharpier on the full output directory const csharpierStartTime = Date.now(); await this.csharpier(this.absolutePathToOutputDirectory); - this.context.logger.info(`[TIMING] csharpier took ${Date.now() - csharpierStartTime}ms`); - this.context.logger.info(`[TIMING] persist took ${Date.now() - persistStartTime}ms`); + this.context.logger.debug(`[TIMING] csharpier took ${Date.now() - csharpierStartTime}ms`); + this.context.logger.debug(`[TIMING] persist took ${Date.now() - persistStartTime}ms`); } private async createProject({ diff --git a/generators/csharp/model/src/ModelGeneratorCli.ts b/generators/csharp/model/src/ModelGeneratorCli.ts index 215776f4de54..666bc2badf8f 100644 --- a/generators/csharp/model/src/ModelGeneratorCli.ts +++ b/generators/csharp/model/src/ModelGeneratorCli.ts @@ -59,7 +59,7 @@ export class ModelGeneratorCLI extends AbstractCsharpGeneratorCli { private async generate(context: ModelGeneratorContext): Promise { const generateStartTime = Date.now(); const generatedTypes = generateModels({ context }); - context.logger.info(`[TIMING] generateModels took ${Date.now() - generateStartTime}ms`); + context.logger.debug(`[TIMING] generateModels took ${Date.now() - generateStartTime}ms`); for (const file of generatedTypes) { context.project.addSourceFiles(file); } @@ -73,7 +73,7 @@ export class ModelGeneratorCLI extends AbstractCsharpGeneratorCli { } } - context.logger.info(`[TIMING] code generation took ${Date.now() - generateStartTime}ms`); + context.logger.debug(`[TIMING] code generation took ${Date.now() - generateStartTime}ms`); await context.project.persist(); } } diff --git a/generators/csharp/model/versions.yml b/generators/csharp/model/versions.yml index b5d882115177..b483cf972f67 100644 --- a/generators/csharp/model/versions.yml +++ b/generators/csharp/model/versions.yml @@ -1,4 +1,13 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 0.0.8 + changelogEntry: + - summary: | + Change [TIMING] log statements from info to debug level so they are only + printed when debug logging is enabled. + type: fix + createdAt: "2026-02-24" + irVersion: 62 + - version: 0.0.7 changelogEntry: - summary: | diff --git a/generators/csharp/sdk/src/SdkGeneratorCli.ts b/generators/csharp/sdk/src/SdkGeneratorCli.ts index 1f4566d6d38a..31682a026779 100644 --- a/generators/csharp/sdk/src/SdkGeneratorCli.ts +++ b/generators/csharp/sdk/src/SdkGeneratorCli.ts @@ -106,13 +106,13 @@ export class SdkGeneratorCLI extends AbstractCsharpGeneratorCli { // generate names for everything up front. const precalculateStartTime = Date.now(); context.precalculate(); - context.logger.info(`[TIMING] precalculate took ${Date.now() - precalculateStartTime}ms`); + context.logger.debug(`[TIMING] precalculate took ${Date.now() - precalculateStartTime}ms`); // before generating anything, generate the models first so that we // can identify collisions or ambiguities in the generated code. const modelsStartTime = Date.now(); const models = generateModels({ context }); - context.logger.info(`[TIMING] generateModels took ${Date.now() - modelsStartTime}ms`); + context.logger.debug(`[TIMING] generateModels took ${Date.now() - modelsStartTime}ms`); await context.snippetGenerator.populateSnippetsCache(); @@ -296,7 +296,7 @@ export class SdkGeneratorCLI extends AbstractCsharpGeneratorCli { context.logger.warn("Failed to generate reference.md, this is OK."); } } - context.logger.info(`[TIMING] code generation took ${Date.now() - generateStartTime}ms`); + context.logger.debug(`[TIMING] code generation took ${Date.now() - generateStartTime}ms`); await context.project.persist(); } diff --git a/generators/csharp/sdk/versions.yml b/generators/csharp/sdk/versions.yml index c30b34404c45..97aa0e838a2d 100644 --- a/generators/csharp/sdk/versions.yml +++ b/generators/csharp/sdk/versions.yml @@ -1,4 +1,13 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 2.22.2 + changelogEntry: + - summary: | + Change [TIMING] log statements from info to debug level so they are only + printed when debug logging is enabled. + type: fix + createdAt: "2026-02-24" + irVersion: 62 + - version: 2.22.1 changelogEntry: - summary: | diff --git a/generators/typescript/sdk/versions.yml b/generators/typescript/sdk/versions.yml index 6e68351124f5..1689affebade 100644 --- a/generators/typescript/sdk/versions.yml +++ b/generators/typescript/sdk/versions.yml @@ -1,4 +1,13 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 3.49.1 + changelogEntry: + - summary: | + Change [TIMING] log statements from info to debug level so they are only + printed when debug logging is enabled. + type: fix + createdAt: "2026-02-24" + irVersion: 63 + - version: 3.49.0 changelogEntry: - summary: | diff --git a/generators/typescript/utils/abstract-generator-cli/src/AbstractGeneratorCli.ts b/generators/typescript/utils/abstract-generator-cli/src/AbstractGeneratorCli.ts index 3a59a16e08d2..32dc884791e5 100644 --- a/generators/typescript/utils/abstract-generator-cli/src/AbstractGeneratorCli.ts +++ b/generators/typescript/utils/abstract-generator-cli/src/AbstractGeneratorCli.ts @@ -126,7 +126,7 @@ export abstract class AbstractGeneratorCli { generatorContext, intermediateRepresentation: ir }); - logger.info(`[TIMING] code generation took ${Date.now() - codeGenStartTime}ms`); + logger.debug(`[TIMING] code generation took ${Date.now() - codeGenStartTime}ms`); if (!generatorContext.didSucceed()) { throw new Error("Failed to generate TypeScript project."); } diff --git a/generators/typescript/utils/commons/src/typescript-project/PersistedTypescriptProject.ts b/generators/typescript/utils/commons/src/typescript-project/PersistedTypescriptProject.ts index f14139371a16..368726322c22 100644 --- a/generators/typescript/utils/commons/src/typescript-project/PersistedTypescriptProject.ts +++ b/generators/typescript/utils/commons/src/typescript-project/PersistedTypescriptProject.ts @@ -81,7 +81,7 @@ export class PersistedTypescriptProject { logger.debug("Running npm pkg fix to normalize package.json"); const startTime = Date.now(); await npm(["pkg", "fix"]); - logger.info(`[TIMING] fixPackageJson took ${Date.now() - startTime}ms`); + logger.debug(`[TIMING] fixPackageJson took ${Date.now() - startTime}ms`); } catch (e) { logger.warn(`Failed to run npm pkg fix: ${e}`); } @@ -111,7 +111,7 @@ export class PersistedTypescriptProject { PNPM_FROZEN_LOCKFILE: "false" } })); - logger.info(`[TIMING] generateLockfile took ${Date.now() - startTime}ms`); + logger.debug(`[TIMING] generateLockfile took ${Date.now() - startTime}ms`); } public async installDependencies(logger: Logger): Promise { @@ -138,7 +138,7 @@ export class PersistedTypescriptProject { PNPM_FROZEN_LOCKFILE: "false" } })); - logger.info(`[TIMING] installDependencies took ${Date.now() - startTime}ms`); + logger.debug(`[TIMING] installDependencies took ${Date.now() - startTime}ms`); } public async format(logger: Logger): Promise { @@ -153,7 +153,7 @@ export class PersistedTypescriptProject { try { const startTime = Date.now(); await pm(this.formatCommand); - logger.info(`[TIMING] format took ${Date.now() - startTime}ms`); + logger.debug(`[TIMING] format took ${Date.now() - startTime}ms`); } catch (e) { logger.error(`Failed to format the generated project: ${e}`); } @@ -172,7 +172,7 @@ export class PersistedTypescriptProject { try { const startTime = Date.now(); await pm(this.checkFixCommand); - logger.info(`[TIMING] checkFix took ${Date.now() - startTime}ms`); + logger.debug(`[TIMING] checkFix took ${Date.now() - startTime}ms`); } catch (e) { logger.error(`Failed to format the generated project: ${e}`); } @@ -189,7 +189,7 @@ export class PersistedTypescriptProject { }); const startTime = Date.now(); await pm(this.buildCommand); - logger.info(`[TIMING] build took ${Date.now() - startTime}ms`); + logger.debug(`[TIMING] build took ${Date.now() - startTime}ms`); } public async copyProjectTo({ @@ -370,7 +370,7 @@ export class PersistedTypescriptProject { await npm(publishCommand, { secrets: [publishInfo.registryUrl] }); - logger.info(`[TIMING] publish took ${Date.now() - startTime}ms`); + logger.debug(`[TIMING] publish took ${Date.now() - startTime}ms`); } public async deleteGitIgnoredFiles(logger: Logger): Promise { From 17acfe4b4784c70741bad6bba78809eb63942859 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:08:13 -0500 Subject: [PATCH 06/10] fix(cli): handle new SDK repos in AUTO versioning (#12688) Co-authored-by: unknown <> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- packages/cli/cli/versions.yml | 10 + .../src/AutoVersioningService.ts | 11 +- .../src/LocalTaskHandler.ts | 16 + .../__test__/AutoVersioningService.test.ts | 433 +++++++++++++++++- 4 files changed, 448 insertions(+), 22 deletions(-) diff --git a/packages/cli/cli/versions.yml b/packages/cli/cli/versions.yml index b5f5986613bc..e23dc4a32705 100644 --- a/packages/cli/cli/versions.yml +++ b/packages/cli/cli/versions.yml @@ -1,4 +1,14 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 3.85.1 + changelogEntry: + - summary: | + Fix AUTO versioning failing for new SDK repositories. When generating an SDK + into an empty or newly initialized repository, the version extraction would fail + because all files are new additions with no previous version lines in the diff. + Now returns 0.0.1 as the initial version instead of throwing an error. + type: fix + createdAt: "2026-02-24" + irVersion: 65 - version: 3.85.0 changelogEntry: - summary: | diff --git a/packages/cli/generation/local-generation/local-workspace-runner/src/AutoVersioningService.ts b/packages/cli/generation/local-generation/local-workspace-runner/src/AutoVersioningService.ts index 0614d22b9fb4..ebe1fde03950 100644 --- a/packages/cli/generation/local-generation/local-workspace-runner/src/AutoVersioningService.ts +++ b/packages/cli/generation/local-generation/local-workspace-runner/src/AutoVersioningService.ts @@ -47,10 +47,10 @@ export class AutoVersioningService { * * @param diffContent The git diff content * @param mappedMagicVersion The magic version after language transformations (e.g., "v505.503.4455" for Go) - * @return The previous version if found - * @throws AutoVersioningException if no previous version can be extracted from the diff + * @return The previous version if found, or undefined if all magic version occurrences are in new files + * @throws AutoVersioningException if the magic version is not found at all in the diff */ - public extractPreviousVersion(diffContent: string, mappedMagicVersion: string): string { + public extractPreviousVersion(diffContent: string, mappedMagicVersion: string): string | undefined { const lines = diffContent.split("\n"); let currentFile = "unknown"; @@ -89,10 +89,11 @@ export class AutoVersioningService { } if (magicVersionOccurrences > 0) { - throw new AutoVersioningException( + this.logger.info( `Found placeholder version in the diff but no matching previous version lines were found in any hunk. ` + - `This may indicate new files or a format change. occurrences=${magicVersionOccurrences}, pairsFound=0` + `This indicates a new SDK repository with no previous version. occurrences=${magicVersionOccurrences}` ); + return undefined; } throw new AutoVersioningException( diff --git a/packages/cli/generation/local-generation/local-workspace-runner/src/LocalTaskHandler.ts b/packages/cli/generation/local-generation/local-workspace-runner/src/LocalTaskHandler.ts index c465fea3e5f5..eacced847a82 100644 --- a/packages/cli/generation/local-generation/local-workspace-runner/src/LocalTaskHandler.ts +++ b/packages/cli/generation/local-generation/local-workspace-runner/src/LocalTaskHandler.ts @@ -139,6 +139,22 @@ export class LocalTaskHandler { this.context.logger.debug(`Generated diff size: ${diffContent.length} bytes`); this.context.logger.debug(`Cleaned diff size: ${cleanedDiff.length} bytes`); + + // Handle new SDK repository with no previous version + if (previousVersion == null) { + this.context.logger.info( + "No previous version found (new SDK repository). Using 0.0.1 as initial version." + ); + const initialVersion = this.version?.startsWith("v") ? "v0.0.1" : "0.0.1"; + const commitMessage = this.isWhitelabel + ? "Initial SDK generation" + : "Initial SDK generation\n\n🌿 Generated with Fern"; + return { + version: initialVersion, + commitMessage + }; + } + this.context.logger.debug(`Previous version detected: ${previousVersion}`); // Call AI to analyze the diff diff --git a/packages/cli/generation/local-generation/local-workspace-runner/src/__test__/AutoVersioningService.test.ts b/packages/cli/generation/local-generation/local-workspace-runner/src/__test__/AutoVersioningService.test.ts index 0fb8090c6ef2..dd5ffcc1508b 100644 --- a/packages/cli/generation/local-generation/local-workspace-runner/src/__test__/AutoVersioningService.test.ts +++ b/packages/cli/generation/local-generation/local-workspace-runner/src/__test__/AutoVersioningService.test.ts @@ -99,7 +99,7 @@ describe("AutoVersioningService", () => { expect(previousVersion).toBe("3.0.0-beta.2"); }); - it("testExtractPreviousVersion_noPreviousVersion", () => { + it("testExtractPreviousVersion_noPreviousVersion_returnsUndefined", () => { const diff = "diff --git a/new-file.txt b/new-file.txt\n" + "new file mode 100644\n" + @@ -109,9 +109,45 @@ describe("AutoVersioningService", () => { "@@ -0,0 +1 @@\n" + "+version = 505.503.4455\n"; - expect(() => { - new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion(diff, "505.503.4455"); - }).toThrow(AutoVersioningException); + const result = new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion(diff, "505.503.4455"); + expect(result).toBeUndefined(); + }); + + it("testExtractPreviousVersion_newRepoWithMultipleNewFiles_returnsUndefined", () => { + // Simulates a new SDK repo where all files are new (like immix-ts-sdk initial generation) + const diff = + "diff --git a/package.json b/package.json\n" + + "new file mode 100644\n" + + "index 0000000..abc123\n" + + "--- /dev/null\n" + + "+++ b/package.json\n" + + "@@ -0,0 +1,6 @@\n" + + "+{\n" + + '+ "name": "test-sdk",\n' + + '+ "version": "505.503.4455",\n' + + '+ "private": false\n' + + "+}\n" + + "diff --git a/src/Client.ts b/src/Client.ts\n" + + "new file mode 100644\n" + + "index 0000000..def456\n" + + "--- /dev/null\n" + + "+++ b/src/Client.ts\n" + + "@@ -0,0 +1,5 @@\n" + + "+export class Client {\n" + + '+ private readonly version = "505.503.4455";\n' + + "+ constructor() {}\n" + + "+}\n" + + "diff --git a/src/core/index.ts b/src/core/index.ts\n" + + "new file mode 100644\n" + + "index 0000000..789abc\n" + + "--- /dev/null\n" + + "+++ b/src/core/index.ts\n" + + "@@ -0,0 +1,3 @@\n" + + '+export const SDK_VERSION = "505.503.4455";\n' + + '+export const SDK_NAME = "test-sdk";\n'; + + const result = new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion(diff, "505.503.4455"); + expect(result).toBeUndefined(); }); it("testCleanDiffForAI_removesMagicVersionLines", () => { @@ -222,7 +258,7 @@ describe("AutoVersioningService", () => { expect(cleaned).not.toContain("version.go"); }); - it("testExtractPreviousVersion_invalidVersionFormat", () => { + it("testExtractPreviousVersion_invalidVersionFormat_returnsUndefined", () => { const diff = "diff --git a/config.txt b/config.txt\n" + "index abc123..def456 100644\n" + @@ -232,9 +268,10 @@ describe("AutoVersioningService", () => { "-some random text\n" + "+magic version is 505.503.4455\n"; - expect(() => { - new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion(diff, "505.503.4455"); - }).toThrow(AutoVersioningException); + // The minus line doesn't form a version pair, so no previous version can be extracted. + // This is treated like a new file scenario - returns undefined instead of throwing. + const result = new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion(diff, "505.503.4455"); + expect(result).toBeUndefined(); }); it("testExtractPreviousVersion_withNonVersionLinesInBetween", () => { @@ -285,7 +322,7 @@ describe("AutoVersioningService", () => { expect(previousVersion).toBe("1.5.0"); }); - it("testExtractPreviousVersion_noMatchingMinusLines_throwsException", () => { + it("testExtractPreviousVersion_noMatchingMinusLines_returnsUndefined", () => { const diff = "diff --git a/README.md b/README.md\n" + "index abc123..def456 100644\n" + @@ -302,14 +339,8 @@ describe("AutoVersioningService", () => { " # Changelog\n" + "+## 505.503.4455\n"; - try { - new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion(diff, "505.503.4455"); - expect.fail("Should have thrown AutoVersioningException"); - } catch (error) { - expect(error).toBeInstanceOf(AutoVersioningException); - expect((error as Error).message).toContain("no matching previous version lines were found"); - expect((error as Error).message).toContain("occurrences=2"); - } + const result = new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion(diff, "505.503.4455"); + expect(result).toBeUndefined(); }); it("testCleanDiffForAI_removesFileWithOnlyVersionChanges", () => { @@ -722,6 +753,374 @@ describe("AutoVersioningService", () => { } }); + // --- Tests for extractPreviousVersion: exception path --- + + it("testExtractPreviousVersion_noMagicVersionInDiff_throws", () => { + // When the magic version is not found at all in the diff, it should throw + const diff = + "diff --git a/README.md b/README.md\n" + + "index abc123..def456 100644\n" + + "--- a/README.md\n" + + "+++ b/README.md\n" + + "@@ -1,3 +1,4 @@\n" + + " # My Project\n" + + "+Some new content\n" + + " ## Installation\n"; + + expect(() => + new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion(diff, "505.503.4455") + ).toThrow(AutoVersioningException); + }); + + it("testExtractPreviousVersion_emptyDiff_throws", () => { + expect(() => + new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion("", "505.503.4455") + ).toThrow(AutoVersioningException); + }); + + it("testExtractPreviousVersion_magicVersionOnlyInContextLines_throws", () => { + // Magic version appears in context lines (no + or - prefix), should not count + const diff = + "diff --git a/config.txt b/config.txt\n" + + "index abc123..def456 100644\n" + + "--- a/config.txt\n" + + "+++ b/config.txt\n" + + "@@ -1,3 +1,4 @@\n" + + " version = 505.503.4455\n" + + "+new line added\n" + + " other content\n"; + + expect(() => + new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion(diff, "505.503.4455") + ).toThrow(AutoVersioningException); + }); + + // --- Tests for extractPreviousVersion: new repo with v prefix --- + + it("testExtractPreviousVersion_newRepoWithVPrefixMagicVersion_returnsUndefined", () => { + // Go SDK new repo: all files are new, magic version has v prefix + const diff = + "diff --git a/version.go b/version.go\n" + + "new file mode 100644\n" + + "index 0000000..abc123\n" + + "--- /dev/null\n" + + "+++ b/version.go\n" + + "@@ -0,0 +1,3 @@\n" + + "+package sdk\n" + + '+const Version = "v505.503.4455"\n' + + "+\n" + + "diff --git a/client.go b/client.go\n" + + "new file mode 100644\n" + + "index 0000000..def456\n" + + "--- /dev/null\n" + + "+++ b/client.go\n" + + "@@ -0,0 +1,5 @@\n" + + "+package sdk\n" + + "+\n" + + "+type Client struct {\n" + + "+ version string // v505.503.4455\n" + + "+}\n"; + + const result = new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion(diff, "v505.503.4455"); + expect(result).toBeUndefined(); + }); + + it("testExtractPreviousVersion_singleNewFileWithVPrefix_returnsUndefined", () => { + const diff = + "diff --git a/version.go b/version.go\n" + + "new file mode 100644\n" + + "index 0000000..abc123\n" + + "--- /dev/null\n" + + "+++ b/version.go\n" + + "@@ -0,0 +1 @@\n" + + '+const Version = "v505.503.4455"\n'; + + const result = new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion(diff, "v505.503.4455"); + expect(result).toBeUndefined(); + }); + + // --- Tests for extractPreviousVersion: mixed scenario --- + + it("testExtractPreviousVersion_mixedNewAndExistingFiles_returnsVersionFromExisting", () => { + // Some files are new (no matching minus lines), but one existing file has a version pair + const diff = + "diff --git a/src/newFile.ts b/src/newFile.ts\n" + + "new file mode 100644\n" + + "index 0000000..abc123\n" + + "--- /dev/null\n" + + "+++ b/src/newFile.ts\n" + + "@@ -0,0 +1,3 @@\n" + + '+export const SDK_VERSION = "505.503.4455";\n' + + "+export const name = 'test';\n" + + "diff --git a/package.json b/package.json\n" + + "index abc123..def456 100644\n" + + "--- a/package.json\n" + + "+++ b/package.json\n" + + "@@ -1,5 +1,5 @@\n" + + " {\n" + + '- "version": "2.3.4",\n' + + '+ "version": "505.503.4455",\n' + + ' "name": "test"\n' + + " }\n"; + + const previousVersion = new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion( + diff, + "505.503.4455" + ); + expect(previousVersion).toBe("2.3.4"); + }); + + it("testExtractPreviousVersion_mixedNewAndExistingFiles_newFileFirst_returnsVersionFromExisting", () => { + // New file appears first in the diff, followed by existing file with version pair. + // The new file's magic version occurrence should be skipped, then find match in existing file. + const diff = + "diff --git a/src/version.ts b/src/version.ts\n" + + "new file mode 100644\n" + + "index 0000000..abc123\n" + + "--- /dev/null\n" + + "+++ b/src/version.ts\n" + + "@@ -0,0 +1,1 @@\n" + + '+export const VERSION = "505.503.4455";\n' + + "diff --git a/src/Client.ts b/src/Client.ts\n" + + "new file mode 100644\n" + + "index 0000000..def456\n" + + "--- /dev/null\n" + + "+++ b/src/Client.ts\n" + + "@@ -0,0 +1,2 @@\n" + + '+import { VERSION } from "./version";\n' + + '+const v = "505.503.4455";\n' + + "diff --git a/package.json b/package.json\n" + + "index 111111..222222 100644\n" + + "--- a/package.json\n" + + "+++ b/package.json\n" + + "@@ -2,3 +2,3 @@\n" + + ' "name": "my-sdk",\n' + + '- "version": "1.0.0",\n' + + '+ "version": "505.503.4455",\n' + + ' "private": false\n'; + + const previousVersion = new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion( + diff, + "505.503.4455" + ); + expect(previousVersion).toBe("1.0.0"); + }); + + // --- Tests for cleanDiffForAI: new file scenarios --- + + it("testCleanDiffForAI_allNewFiles_removesAllVersionLines", () => { + // Simulates a new SDK repo where all files are new (all + lines) + const diff = + "diff --git a/package.json b/package.json\n" + + "new file mode 100644\n" + + "index 0000000..abc123\n" + + "--- /dev/null\n" + + "+++ b/package.json\n" + + "@@ -0,0 +1,5 @@\n" + + "+{\n" + + '+ "name": "test-sdk",\n' + + '+ "version": "505.503.4455",\n' + + '+ "private": false\n' + + "+}\n" + + "diff --git a/src/Client.ts b/src/Client.ts\n" + + "new file mode 100644\n" + + "index 0000000..def456\n" + + "--- /dev/null\n" + + "+++ b/src/Client.ts\n" + + "@@ -0,0 +1,3 @@\n" + + "+export class Client {\n" + + "+ constructor() {}\n" + + "+}\n"; + + const cleaned = new AutoVersioningService({ logger: mockLogger }).cleanDiffForAI(diff, "505.503.4455"); + + // The version line should be removed + expect(cleaned).not.toContain("505.503.4455"); + // Other new file content should be preserved + expect(cleaned).toContain('+ "name": "test-sdk"'); + expect(cleaned).toContain("+export class Client"); + }); + + it("testCleanDiffForAI_newFileWithVPrefixMagicVersion", () => { + // Go SDK new file scenario with v-prefixed magic version + const diff = + "diff --git a/version.go b/version.go\n" + + "new file mode 100644\n" + + "index 0000000..abc123\n" + + "--- /dev/null\n" + + "+++ b/version.go\n" + + "@@ -0,0 +1,3 @@\n" + + "+package sdk\n" + + '+const Version = "v505.503.4455"\n' + + "+\n"; + + const cleaned = new AutoVersioningService({ logger: mockLogger }).cleanDiffForAI(diff, "v505.503.4455"); + + expect(cleaned).not.toContain("v505.503.4455"); + expect(cleaned).toContain("+package sdk"); + }); + + // --- Tests for replaceMagicVersion: initial version scenarios --- + + it("testReplaceMagicVersion_withVPrefixInitialVersion", async () => { + // Simulates Go SDK initial generation: replacing v505.503.4455 with v0.0.1 + const tempDir = await fs.mkdtemp(path.join(require("os").tmpdir(), "test-")); + try { + const versionFile = path.join(tempDir, "version.go"); + await fs.writeFile(versionFile, 'package sdk\n\nconst Version = "v505.503.4455"\n'); + + const clientFile = path.join(tempDir, "client.go"); + await fs.writeFile(clientFile, 'package sdk\n\nvar sdkVersion = "v505.503.4455"\n'); + + await new AutoVersioningService({ logger: mockLogger }).replaceMagicVersion( + tempDir, + "v505.503.4455", + "v0.0.1" + ); + + const versionContent = await fs.readFile(versionFile, "utf-8"); + expect(versionContent).not.toContain("v505.503.4455"); + expect(versionContent).toContain("v0.0.1"); + expect(versionContent).toBe('package sdk\n\nconst Version = "v0.0.1"\n'); + + const clientContent = await fs.readFile(clientFile, "utf-8"); + expect(clientContent).not.toContain("v505.503.4455"); + expect(clientContent).toContain("v0.0.1"); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); + + it("testReplaceMagicVersion_withNonVPrefixInitialVersion", async () => { + // Simulates TS/Python SDK initial generation: replacing 505.503.4455 with 0.0.1 + const tempDir = await fs.mkdtemp(path.join(require("os").tmpdir(), "test-")); + try { + const packageJson = path.join(tempDir, "package.json"); + await fs.writeFile(packageJson, '{\n "name": "test-sdk",\n "version": "505.503.4455"\n}'); + + const clientFile = path.join(tempDir, "src"); + await fs.mkdir(clientFile, { recursive: true }); + const versionTs = path.join(clientFile, "version.ts"); + await fs.writeFile(versionTs, 'export const SDK_VERSION = "505.503.4455";\n'); + + await new AutoVersioningService({ logger: mockLogger }).replaceMagicVersion( + tempDir, + "505.503.4455", + "0.0.1" + ); + + const packageContent = await fs.readFile(packageJson, "utf-8"); + expect(packageContent).not.toContain("505.503.4455"); + expect(packageContent).toContain("0.0.1"); + expect(packageContent).toBe('{\n "name": "test-sdk",\n "version": "0.0.1"\n}'); + + const versionContent = await fs.readFile(versionTs, "utf-8"); + expect(versionContent).not.toContain("505.503.4455"); + expect(versionContent).toContain("0.0.1"); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); + + // --- Tests for extractPreviousVersion: edge cases --- + + it("testExtractPreviousVersion_magicVersionInDeletedLine_throws", () => { + // Magic version appears only in a deleted line (not added), should not count + const diff = + "diff --git a/package.json b/package.json\n" + + "index abc123..def456 100644\n" + + "--- a/package.json\n" + + "+++ b/package.json\n" + + "@@ -1,5 +1,4 @@\n" + + " {\n" + + '- "version": "505.503.4455",\n' + + ' "name": "test"\n' + + " }\n"; + + expect(() => + new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion(diff, "505.503.4455") + ).toThrow(AutoVersioningException); + }); + + it("testExtractPreviousVersion_multipleExistingFilesWithVersions_returnsFirst", () => { + // Multiple files with version pairs - should return the first match + const diff = + "diff --git a/package.json b/package.json\n" + + "index abc123..def456 100644\n" + + "--- a/package.json\n" + + "+++ b/package.json\n" + + "@@ -1,5 +1,5 @@\n" + + " {\n" + + '- "version": "3.2.1",\n' + + '+ "version": "505.503.4455",\n' + + ' "name": "test"\n' + + " }\n" + + "diff --git a/src/version.ts b/src/version.ts\n" + + "index 111111..222222 100644\n" + + "--- a/src/version.ts\n" + + "+++ b/src/version.ts\n" + + "@@ -1 +1 @@\n" + + '-export const VERSION = "3.2.1";\n' + + '+export const VERSION = "505.503.4455";\n'; + + const previousVersion = new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion( + diff, + "505.503.4455" + ); + expect(previousVersion).toBe("3.2.1"); + }); + + it("testExtractPreviousVersion_versionWithBuildMetadata", () => { + const diff = + "diff --git a/setup.py b/setup.py\n" + + "index abc123..def456 100644\n" + + "--- a/setup.py\n" + + "+++ b/setup.py\n" + + "@@ -1,3 +1,3 @@\n" + + " setup(\n" + + "- version='1.0.0+build.123',\n" + + "+ version='505.503.4455',\n" + + " )\n"; + + const previousVersion = new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion( + diff, + "505.503.4455" + ); + expect(previousVersion).toBe("1.0.0+build.123"); + }); + + // --- Tests for cleanDiffForAI: mixed new and existing files --- + + it("testCleanDiffForAI_mixedNewAndExistingFiles_preservesNonVersionContent", () => { + const diff = + "diff --git a/src/newFile.ts b/src/newFile.ts\n" + + "new file mode 100644\n" + + "index 0000000..abc123\n" + + "--- /dev/null\n" + + "+++ b/src/newFile.ts\n" + + "@@ -0,0 +1,3 @@\n" + + "+export function hello() {\n" + + '+ return "505.503.4455";\n' + + "+}\n" + + "diff --git a/src/existing.ts b/src/existing.ts\n" + + "index 111111..222222 100644\n" + + "--- a/src/existing.ts\n" + + "+++ b/src/existing.ts\n" + + "@@ -1,3 +1,4 @@\n" + + " export class Existing {\n" + + "+ newMethod() { return true; }\n" + + " }\n"; + + const cleaned = new AutoVersioningService({ logger: mockLogger }).cleanDiffForAI(diff, "505.503.4455"); + + // Version line removed from new file + expect(cleaned).not.toContain("505.503.4455"); + // Non-version new file content preserved + expect(cleaned).toContain("+export function hello()"); + // Existing file changes preserved + expect(cleaned).toContain("+ newMethod() { return true; }"); + }); + // TODO(tjb9dc): Reenable these tests, need to have them reference the LocalTaskHandler's gitDiff function (or refactor) // it("testAutoVersioningWorkflow_endToEnd", async () => { // const tempDir = await fs.mkdtemp(path.join(require("os").tmpdir(), "test-")); From e62e182b7d091d3c769d991649c30426652d92f6 Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Mon, 23 Feb 2026 22:20:56 -0500 Subject: [PATCH 07/10] fix(cli): restore deleted changelog entries for 3.82.0 and 3.81.1 (#12692) Co-Authored-By: unknown <> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- packages/cli/cli/versions.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/cli/cli/versions.yml b/packages/cli/cli/versions.yml index e23dc4a32705..a68bfab7c4b5 100644 --- a/packages/cli/cli/versions.yml +++ b/packages/cli/cli/versions.yml @@ -113,6 +113,22 @@ extension (in OpenAPI) with algorithm, encoding, header name, and payload format. SDK generators can use this to produce signature verification utilities. type: feat + - summary: | + Add `terminator` field support for SSE streaming throughout the OpenAPI to Fern pipeline. + The field was already present in the Fern definition schema but missing from the OpenAPI + extension parsing, OpenAPI-IR schema, and the OpenAPI-IR to Fern conversion layers. + type: feat + createdAt: "2026-02-19" + irVersion: 65 +- version: 3.81.1 + changelogEntry: + - summary: | + Fix example object keys starting with `$` (e.g. `$ref`) not being unescaped when + generating the IR `jsonExample`. The Fern definition escapes `$`-prefixed keys with + a backslash to avoid collision with example references, but the backslash was not + removed when building the wire-format JSON example, causing generators to emit + invalid code (e.g. `"\$ref"` in Go). + type: fix createdAt: "2026-02-19" irVersion: 65 - version: 3.81.0 From 018350de765fb8716b0ff194186a9afff3c2f3b1 Mon Sep 17 00:00:00 2001 From: Fern Support <126544928+fern-support@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:41:38 -0500 Subject: [PATCH 08/10] chore(cli): update SDK versions in pnpm-workspace.yaml to 1.0.x for consistency and stability (#12691) Co-authored-by: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> --- fern/apis/docs-yml/generators.yml | 2 +- fern/apis/fern-definition/generators.yml | 2 +- fern/apis/generator-cli/generators.yml | 2 +- fern/apis/generators-yml/generators.yml | 2 +- generators/postman/fern/generators.yml | 1 + .../conjure/conjure-sdk/fern/generators.yml | 2 +- .../src/sdk/core/fetcher/Fetcher.ts | 8 +- .../sdk/core/schemas/builders/list/list.ts | 38 +- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 +++++-- .../core/schemas/builders/record/record.ts | 67 ++- .../sdk/core/schemas/builders/union/union.ts | 11 +- .../sdk/core/schemas/utils/isPlainObject.ts | 11 +- .../openapi/openapi-ir/fern/generators.yml | 2 +- .../src/sdk/core/fetcher/Fetcher.ts | 8 +- .../sdk/core/schemas/builders/list/list.ts | 38 +- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 +++++-- .../core/schemas/builders/record/record.ts | 67 ++- .../sdk/core/schemas/builders/union/union.ts | 11 +- .../sdk/core/schemas/utils/isPlainObject.ts | 11 +- packages/cli/cli/versions.yml | 9 +- .../schemas/sdk/core/fetcher/Fetcher.ts | 8 +- .../sdk/core/schemas/builders/list/list.ts | 38 +- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 +++++-- .../core/schemas/builders/record/record.ts | 67 ++- .../sdk/core/schemas/builders/union/union.ts | 11 +- .../sdk/core/schemas/utils/isPlainObject.ts | 11 +- .../schemas/core/fetcher/Fetcher.ts | 8 +- .../core/schemas/builders/list/list.ts | 38 +- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 +++++-- .../core/schemas/builders/record/record.ts | 67 ++- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../generators/types/GraphQlSpecSchema.ts | 4 +- .../types/ExampleBodyResponseSchema.ts | 2 +- .../types/ExampleEndpointCallSchema.ts | 2 +- .../examples/types/ExampleSseEventSchema.ts | 2 +- .../src/schemas/core/fetcher/Fetcher.ts | 8 +- .../core/schemas/builders/list/list.ts | 38 +- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 +++++-- .../core/schemas/builders/record/record.ts | 67 ++- .../core/schemas/builders/union/union.ts | 11 +- .../core/schemas/utils/isPlainObject.ts | 11 +- .../cli/generation/ir-migrations/package.json | 2 +- .../configuration/sdk/core/fetcher/Fetcher.ts | 8 +- .../sdk/core/schemas/builders/list/list.ts | 38 +- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 +++++-- .../core/schemas/builders/record/record.ts | 67 ++- .../sdk/core/schemas/builders/union/union.ts | 11 +- .../sdk/core/schemas/utils/isPlainObject.ts | 11 +- .../fern/apis/ir-types-latest/generators.yml | 6 +- .../fern/apis/ir-types-v23/generators.yml | 2 +- .../fern/apis/ir-types-v24/generators.yml | 2 +- .../fern/apis/ir-types-v25/generators.yml | 2 +- .../fern/apis/ir-types-v26/generators.yml | 2 +- .../fern/apis/ir-types-v27/generators.yml | 2 +- .../fern/apis/ir-types-v28/generators.yml | 2 +- .../fern/apis/ir-types-v29/generators.yml | 2 +- .../fern/apis/ir-types-v30/generators.yml | 2 +- .../fern/apis/ir-types-v31/generators.yml | 2 +- .../fern/apis/ir-types-v32/generators.yml | 4 +- .../fern/apis/ir-types-v33/generators.yml | 4 +- .../fern/apis/ir-types-v34/generators.yml | 4 +- .../fern/apis/ir-types-v35/generators.yml | 4 +- .../fern/apis/ir-types-v36/generators.yml | 4 +- .../fern/apis/ir-types-v37/generators.yml | 4 +- .../fern/apis/ir-types-v38/generators.yml | 4 +- .../fern/apis/ir-types-v39/generators.yml | 4 +- .../fern/apis/ir-types-v40/generators.yml | 4 +- .../fern/apis/ir-types-v41/generators.yml | 4 +- .../fern/apis/ir-types-v42/generators.yml | 4 +- .../fern/apis/ir-types-v43/generators.yml | 4 +- .../fern/apis/ir-types-v44/generators.yml | 4 +- .../fern/apis/ir-types-v45/generators.yml | 4 +- .../fern/apis/ir-types-v46/generators.yml | 17 +- .../fern/apis/ir-types-v47/generators.yml | 17 +- .../fern/apis/ir-types-v48/generators.yml | 17 +- .../fern/apis/ir-types-v49/generators.yml | 17 +- .../fern/apis/ir-types-v50/generators.yml | 17 +- .../fern/apis/ir-types-v51/generators.yml | 17 +- .../fern/apis/ir-types-v52/generators.yml | 17 +- .../fern/apis/ir-types-v53/generators.yml | 17 +- .../fern/apis/ir-types-v54/generators.yml | 17 +- .../fern/apis/ir-types-v55/generators.yml | 17 +- .../fern/apis/ir-types-v56/generators.yml | 17 +- .../fern/apis/ir-types-v57/generators.yml | 6 +- .../fern/apis/ir-types-v58/generators.yml | 6 +- .../fern/apis/ir-types-v59/generators.yml | 6 +- .../fern/apis/ir-types-v60/generators.yml | 6 +- .../fern/apis/ir-types-v61/generators.yml | 6 +- .../fern/apis/ir-types-v62/generators.yml | 6 +- .../fern/apis/ir-types-v63/generators.yml | 6 +- .../ir-sdk/src/sdk/core/fetcher/Fetcher.ts | 8 +- .../sdk/core/schemas/builders/list/list.ts | 38 +- .../object-like/getObjectLikeUtils.ts | 20 +- .../core/schemas/builders/object/object.ts | 180 +++++-- .../core/schemas/builders/record/record.ts | 67 ++- .../sdk/core/schemas/builders/union/union.ts | 11 +- .../sdk/core/schemas/utils/isPlainObject.ts | 11 +- packages/seed/fern/generators.yml | 2 +- .../resources/config/types/BuildScripts.ts | 8 +- .../types/CliSeedWorkspaceConfiguration.ts | 2 +- .../config/types/FixtureConfigurations.ts | 20 +- .../resources/config/types/LocalBuildInfo.ts | 2 +- .../resources/config/types/PublishCommand.ts | 4 +- .../resources/config/types/PublishDocker.ts | 4 +- .../types/PublishDockerConfiguration.ts | 2 +- .../resources/config/types/ReadmeConfig.ts | 12 +- .../config/types/ReadmeEndpointObject.ts | 2 +- .../api/resources/config/types/Script.ts | 2 +- .../config/types/ScriptCommandsByPhase.ts | 4 +- .../types/SeedWorkspaceConfiguration.ts | 20 +- .../config/types/TestConfiguration.ts | 4 +- .../types/TestContainerConfiguration.ts | 2 +- .../types/FeatureImplementationWithDetails.ts | 2 +- .../seed/src/config/core/fetcher/Fetcher.ts | 8 +- pnpm-lock.yaml | 498 +++++++++--------- pnpm-workspace.yaml | 81 +-- 123 files changed, 2003 insertions(+), 1364 deletions(-) diff --git a/fern/apis/docs-yml/generators.yml b/fern/apis/docs-yml/generators.yml index 11fa34303a72..06357918c58f 100644 --- a/fern/apis/docs-yml/generators.yml +++ b/fern/apis/docs-yml/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.2 + version: 3.49.0 output: location: local-file-system path: ../../../packages/cli/configuration/src/docs-yml/schemas/sdk diff --git a/fern/apis/fern-definition/generators.yml b/fern/apis/fern-definition/generators.yml index 3e4f9890e8c4..6d6809458b99 100644 --- a/fern/apis/fern-definition/generators.yml +++ b/fern/apis/fern-definition/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.2 + version: 3.49.0 output: location: local-file-system path: ../../../packages/cli/fern-definition/schema/src/schemas diff --git a/fern/apis/generator-cli/generators.yml b/fern/apis/generator-cli/generators.yml index f7f3b7b02553..892bec26a9e4 100644 --- a/fern/apis/generator-cli/generators.yml +++ b/fern/apis/generator-cli/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.2 + version: 3.49.0 output: location: local-file-system path: ../../../packages/generator-cli/src/configuration/sdk diff --git a/fern/apis/generators-yml/generators.yml b/fern/apis/generators-yml/generators.yml index 753ff6324f61..48e808fa43a7 100644 --- a/fern/apis/generators-yml/generators.yml +++ b/fern/apis/generators-yml/generators.yml @@ -6,7 +6,7 @@ groups: - generators-yml generators: - name: fernapi/fern-typescript-sdk - version: 3.46.2 + version: 3.49.0 output: location: local-file-system path: ../../../packages/cli/configuration/src/generators-yml/schemas diff --git a/generators/postman/fern/generators.yml b/generators/postman/fern/generators.yml index 247d2368de17..f4aca800d47e 100644 --- a/generators/postman/fern/generators.yml +++ b/generators/postman/fern/generators.yml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json default-group: sdks groups: sdks: diff --git a/packages/cli/api-importers/conjure/conjure-sdk/fern/generators.yml b/packages/cli/api-importers/conjure/conjure-sdk/fern/generators.yml index ec214f9d2642..c6f406e5ecd8 100644 --- a/packages/cli/api-importers/conjure/conjure-sdk/fern/generators.yml +++ b/packages/cli/api-importers/conjure/conjure-sdk/fern/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.2 + version: 3.49.0 output: location: local-file-system path: ../src/sdk diff --git a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/fetcher/Fetcher.ts b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/fetcher/Fetcher.ts index 45cae32b23c1..5e5058a9144f 100644 --- a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/fetcher/Fetcher.ts +++ b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/fetcher/Fetcher.ts @@ -218,7 +218,13 @@ async function getHeaders(args: Fetcher.Args): Promise { newHeaders.set( "Accept", - args.responseType === "json" ? "application/json" : args.responseType === "text" ? "text/plain" : "*/*", + args.responseType === "json" + ? "application/json" + : args.responseType === "text" + ? "text/plain" + : args.responseType === "sse" + ? "text/event-stream" + : "*/*", ); if (args.body !== undefined && args.contentType != null) { newHeaders.set("Content-Type", args.contentType); diff --git a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/list/list.ts b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/list/list.ts +++ b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/object/object.ts b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/object/object.ts +++ b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/record/record.ts b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/record/record.ts +++ b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/union/union.ts b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/union/union.ts +++ b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/utils/isPlainObject.ts b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/utils/isPlainObject.ts +++ b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/packages/cli/api-importers/openapi/openapi-ir/fern/generators.yml b/packages/cli/api-importers/openapi/openapi-ir/fern/generators.yml index 358e67b3b312..fa1694b6fb3a 100644 --- a/packages/cli/api-importers/openapi/openapi-ir/fern/generators.yml +++ b/packages/cli/api-importers/openapi/openapi-ir/fern/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.2 + version: 3.49.0 output: location: local-file-system path: ../src/sdk diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/fetcher/Fetcher.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/fetcher/Fetcher.ts index 45cae32b23c1..5e5058a9144f 100644 --- a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/fetcher/Fetcher.ts +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/fetcher/Fetcher.ts @@ -218,7 +218,13 @@ async function getHeaders(args: Fetcher.Args): Promise { newHeaders.set( "Accept", - args.responseType === "json" ? "application/json" : args.responseType === "text" ? "text/plain" : "*/*", + args.responseType === "json" + ? "application/json" + : args.responseType === "text" + ? "text/plain" + : args.responseType === "sse" + ? "text/event-stream" + : "*/*", ); if (args.body !== undefined && args.contentType != null) { newHeaders.set("Content-Type", args.contentType); diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/list/list.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/list/list.ts +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/object/object.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/object/object.ts +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/record/record.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/record/record.ts +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/union/union.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/union/union.ts +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/utils/isPlainObject.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/utils/isPlainObject.ts +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/packages/cli/cli/versions.yml b/packages/cli/cli/versions.yml index a68bfab7c4b5..0f4d806f4ac3 100644 --- a/packages/cli/cli/versions.yml +++ b/packages/cli/cli/versions.yml @@ -1,4 +1,11 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 3.85.2 + changelogEntry: + - summary: | + Regenerate internal SDKs with the latest TypeScript SDK generator to pick up improvements and fixes. + type: chore + createdAt: "2026-02-23" + irVersion: 65 - version: 3.85.1 changelogEntry: - summary: | @@ -17,7 +24,7 @@ (the Docker Hub org), the CLI now automatically corrects it and prints a warning. This applies to CLI commands (`--generator` flag) and `generators.yml` configuration files. type: feat - createdAt: "2026-02-24" + createdAt: "2026-02-23" irVersion: 65 - version: 3.84.0 changelogEntry: diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/fetcher/Fetcher.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/fetcher/Fetcher.ts index 45cae32b23c1..5e5058a9144f 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/fetcher/Fetcher.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/fetcher/Fetcher.ts @@ -218,7 +218,13 @@ async function getHeaders(args: Fetcher.Args): Promise { newHeaders.set( "Accept", - args.responseType === "json" ? "application/json" : args.responseType === "text" ? "text/plain" : "*/*", + args.responseType === "json" + ? "application/json" + : args.responseType === "text" + ? "text/plain" + : args.responseType === "sse" + ? "text/event-stream" + : "*/*", ); if (args.body !== undefined && args.contentType != null) { newHeaders.set("Content-Type", args.contentType); diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/list/list.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/list/list.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/object/object.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/object/object.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/record/record.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/record/record.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/union/union.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/union/union.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/utils/isPlainObject.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/utils/isPlainObject.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/packages/cli/configuration/src/generators-yml/schemas/core/fetcher/Fetcher.ts b/packages/cli/configuration/src/generators-yml/schemas/core/fetcher/Fetcher.ts index 45cae32b23c1..5e5058a9144f 100644 --- a/packages/cli/configuration/src/generators-yml/schemas/core/fetcher/Fetcher.ts +++ b/packages/cli/configuration/src/generators-yml/schemas/core/fetcher/Fetcher.ts @@ -218,7 +218,13 @@ async function getHeaders(args: Fetcher.Args): Promise { newHeaders.set( "Accept", - args.responseType === "json" ? "application/json" : args.responseType === "text" ? "text/plain" : "*/*", + args.responseType === "json" + ? "application/json" + : args.responseType === "text" + ? "text/plain" + : args.responseType === "sse" + ? "text/event-stream" + : "*/*", ); if (args.body !== undefined && args.contentType != null) { newHeaders.set("Content-Type", args.contentType); diff --git a/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/list/list.ts b/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/list/list.ts +++ b/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/object-like/getObjectLikeUtils.ts b/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/object/object.ts b/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/object/object.ts +++ b/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/record/record.ts b/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/record/record.ts +++ b/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/union/union.ts b/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/union/union.ts +++ b/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/packages/cli/configuration/src/generators-yml/schemas/core/schemas/utils/isPlainObject.ts b/packages/cli/configuration/src/generators-yml/schemas/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/packages/cli/configuration/src/generators-yml/schemas/core/schemas/utils/isPlainObject.ts +++ b/packages/cli/configuration/src/generators-yml/schemas/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/packages/cli/configuration/src/generators-yml/schemas/serialization/resources/generators/types/GraphQlSpecSchema.ts b/packages/cli/configuration/src/generators-yml/schemas/serialization/resources/generators/types/GraphQlSpecSchema.ts index fb2b685392a0..0a00013a1368 100644 --- a/packages/cli/configuration/src/generators-yml/schemas/serialization/resources/generators/types/GraphQlSpecSchema.ts +++ b/packages/cli/configuration/src/generators-yml/schemas/serialization/resources/generators/types/GraphQlSpecSchema.ts @@ -1,8 +1,8 @@ // This file was auto-generated by Fern from our API Definition. -import * as serializers from "../../../index.js"; -import * as GeneratorsYml from "../../../../api/index.js"; +import type * as GeneratorsYml from "../../../../api/index.js"; import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; export const GraphQlSpecSchema: core.serialization.ObjectSchema< serializers.GraphQlSpecSchema.Raw, diff --git a/packages/cli/fern-definition/schema/src/schemas/api/resources/examples/types/ExampleBodyResponseSchema.ts b/packages/cli/fern-definition/schema/src/schemas/api/resources/examples/types/ExampleBodyResponseSchema.ts index 732fecc94f91..007cafe5e252 100644 --- a/packages/cli/fern-definition/schema/src/schemas/api/resources/examples/types/ExampleBodyResponseSchema.ts +++ b/packages/cli/fern-definition/schema/src/schemas/api/resources/examples/types/ExampleBodyResponseSchema.ts @@ -4,5 +4,5 @@ import type * as FernDefinition from "../../../index.js"; export interface ExampleBodyResponseSchema { error?: string; - body?: FernDefinition.ExampleTypeReferenceSchema | undefined; + body?: FernDefinition.ExampleTypeReferenceSchema; } diff --git a/packages/cli/fern-definition/schema/src/schemas/api/resources/examples/types/ExampleEndpointCallSchema.ts b/packages/cli/fern-definition/schema/src/schemas/api/resources/examples/types/ExampleEndpointCallSchema.ts index e68689930b1c..3645f097e416 100644 --- a/packages/cli/fern-definition/schema/src/schemas/api/resources/examples/types/ExampleEndpointCallSchema.ts +++ b/packages/cli/fern-definition/schema/src/schemas/api/resources/examples/types/ExampleEndpointCallSchema.ts @@ -7,7 +7,7 @@ export interface ExampleEndpointCallSchema extends FernDefinition.WithName, Fern "path-parameters"?: Record; "query-parameters"?: Record; headers?: Record; - request?: FernDefinition.ExampleTypeReferenceSchema | undefined; + request?: FernDefinition.ExampleTypeReferenceSchema; response?: FernDefinition.ExampleResponseSchema; "code-samples"?: FernDefinition.ExampleCodeSampleSchema[]; } diff --git a/packages/cli/fern-definition/schema/src/schemas/api/resources/examples/types/ExampleSseEventSchema.ts b/packages/cli/fern-definition/schema/src/schemas/api/resources/examples/types/ExampleSseEventSchema.ts index eacf3c0d6135..26162ce32372 100644 --- a/packages/cli/fern-definition/schema/src/schemas/api/resources/examples/types/ExampleSseEventSchema.ts +++ b/packages/cli/fern-definition/schema/src/schemas/api/resources/examples/types/ExampleSseEventSchema.ts @@ -4,5 +4,5 @@ import type * as FernDefinition from "../../../index.js"; export interface ExampleSseEventSchema { event: string; - data?: FernDefinition.ExampleTypeReferenceSchema | undefined; + data?: FernDefinition.ExampleTypeReferenceSchema; } diff --git a/packages/cli/fern-definition/schema/src/schemas/core/fetcher/Fetcher.ts b/packages/cli/fern-definition/schema/src/schemas/core/fetcher/Fetcher.ts index 45cae32b23c1..5e5058a9144f 100644 --- a/packages/cli/fern-definition/schema/src/schemas/core/fetcher/Fetcher.ts +++ b/packages/cli/fern-definition/schema/src/schemas/core/fetcher/Fetcher.ts @@ -218,7 +218,13 @@ async function getHeaders(args: Fetcher.Args): Promise { newHeaders.set( "Accept", - args.responseType === "json" ? "application/json" : args.responseType === "text" ? "text/plain" : "*/*", + args.responseType === "json" + ? "application/json" + : args.responseType === "text" + ? "text/plain" + : args.responseType === "sse" + ? "text/event-stream" + : "*/*", ); if (args.body !== undefined && args.contentType != null) { newHeaders.set("Content-Type", args.contentType); diff --git a/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/list/list.ts b/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/list/list.ts +++ b/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/object-like/getObjectLikeUtils.ts b/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/object/object.ts b/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/object/object.ts +++ b/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/record/record.ts b/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/record/record.ts +++ b/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/union/union.ts b/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/union/union.ts +++ b/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/packages/cli/fern-definition/schema/src/schemas/core/schemas/utils/isPlainObject.ts b/packages/cli/fern-definition/schema/src/schemas/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/packages/cli/fern-definition/schema/src/schemas/core/schemas/utils/isPlainObject.ts +++ b/packages/cli/fern-definition/schema/src/schemas/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/packages/cli/generation/ir-migrations/package.json b/packages/cli/generation/ir-migrations/package.json index 8227e4b2f729..1c54a4a5a6f3 100644 --- a/packages/cli/generation/ir-migrations/package.json +++ b/packages/cli/generation/ir-migrations/package.json @@ -96,7 +96,7 @@ "@fern-fern/ir-v56-sdk": "catalog:", "@fern-fern/ir-v57-sdk": "catalog:", "@fern-fern/ir-v58-sdk": "catalog:", - "@fern-fern/ir-v59-sdk": "1.0.0", + "@fern-fern/ir-v59-sdk": "catalog:", "@fern-fern/ir-v6-model": "catalog:", "@fern-fern/ir-v60-sdk": "catalog:", "@fern-fern/ir-v61-sdk": "catalog:", diff --git a/packages/generator-cli/src/configuration/sdk/core/fetcher/Fetcher.ts b/packages/generator-cli/src/configuration/sdk/core/fetcher/Fetcher.ts index 45cae32b23c1..5e5058a9144f 100644 --- a/packages/generator-cli/src/configuration/sdk/core/fetcher/Fetcher.ts +++ b/packages/generator-cli/src/configuration/sdk/core/fetcher/Fetcher.ts @@ -218,7 +218,13 @@ async function getHeaders(args: Fetcher.Args): Promise { newHeaders.set( "Accept", - args.responseType === "json" ? "application/json" : args.responseType === "text" ? "text/plain" : "*/*", + args.responseType === "json" + ? "application/json" + : args.responseType === "text" + ? "text/plain" + : args.responseType === "sse" + ? "text/event-stream" + : "*/*", ); if (args.body !== undefined && args.contentType != null) { newHeaders.set("Content-Type", args.contentType); diff --git a/packages/generator-cli/src/configuration/sdk/core/schemas/builders/list/list.ts b/packages/generator-cli/src/configuration/sdk/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/packages/generator-cli/src/configuration/sdk/core/schemas/builders/list/list.ts +++ b/packages/generator-cli/src/configuration/sdk/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/generator-cli/src/configuration/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts b/packages/generator-cli/src/configuration/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/packages/generator-cli/src/configuration/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/packages/generator-cli/src/configuration/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/packages/generator-cli/src/configuration/sdk/core/schemas/builders/object/object.ts b/packages/generator-cli/src/configuration/sdk/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/packages/generator-cli/src/configuration/sdk/core/schemas/builders/object/object.ts +++ b/packages/generator-cli/src/configuration/sdk/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/packages/generator-cli/src/configuration/sdk/core/schemas/builders/record/record.ts b/packages/generator-cli/src/configuration/sdk/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/packages/generator-cli/src/configuration/sdk/core/schemas/builders/record/record.ts +++ b/packages/generator-cli/src/configuration/sdk/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/generator-cli/src/configuration/sdk/core/schemas/builders/union/union.ts b/packages/generator-cli/src/configuration/sdk/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/packages/generator-cli/src/configuration/sdk/core/schemas/builders/union/union.ts +++ b/packages/generator-cli/src/configuration/sdk/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/packages/generator-cli/src/configuration/sdk/core/schemas/utils/isPlainObject.ts b/packages/generator-cli/src/configuration/sdk/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/packages/generator-cli/src/configuration/sdk/core/schemas/utils/isPlainObject.ts +++ b/packages/generator-cli/src/configuration/sdk/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/packages/ir-sdk/fern/apis/ir-types-latest/generators.yml b/packages/ir-sdk/fern/apis/ir-types-latest/generators.yml index 186de7bf070f..c712392d6e15 100644 --- a/packages/ir-sdk/fern/apis/ir-types-latest/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-latest/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: local-file-system path: ../../../src/sdk @@ -20,7 +20,7 @@ groups: - dynamic generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm package-name: "@fern-api/dynamic-ir-sdk" @@ -31,7 +31,7 @@ groups: node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v23/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v23/generators.yml index fa9f46c231b6..6b0a42d5e3d2 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v23/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v23/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 config: includeUtilsOnUnionMembers: true noOptionalProperties: true diff --git a/packages/ir-sdk/fern/apis/ir-types-v24/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v24/generators.yml index 6d210149dd10..1893d1940175 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v24/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v24/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 config: includeUtilsOnUnionMembers: true noOptionalProperties: true diff --git a/packages/ir-sdk/fern/apis/ir-types-v25/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v25/generators.yml index e486218c4cfa..e6a93380340e 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v25/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v25/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 config: includeUtilsOnUnionMembers: true noOptionalProperties: true diff --git a/packages/ir-sdk/fern/apis/ir-types-v26/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v26/generators.yml index a5d22f9ceed2..86d41c0cdf33 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v26/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v26/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 config: includeUtilsOnUnionMembers: true noOptionalProperties: true diff --git a/packages/ir-sdk/fern/apis/ir-types-v27/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v27/generators.yml index ac52d8137798..b0bc4bbf6425 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v27/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v27/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 config: includeUtilsOnUnionMembers: true noOptionalProperties: true diff --git a/packages/ir-sdk/fern/apis/ir-types-v28/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v28/generators.yml index 804389fbf29d..c6c2f045c3fb 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v28/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v28/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 config: includeUtilsOnUnionMembers: true noOptionalProperties: true diff --git a/packages/ir-sdk/fern/apis/ir-types-v29/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v29/generators.yml index 3e883e0f1168..3d87fca69b77 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v29/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v29/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 config: includeUtilsOnUnionMembers: true noOptionalProperties: true diff --git a/packages/ir-sdk/fern/apis/ir-types-v30/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v30/generators.yml index 34d53572df73..97220b985b26 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v30/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v30/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 config: includeUtilsOnUnionMembers: true noOptionalProperties: true diff --git a/packages/ir-sdk/fern/apis/ir-types-v31/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v31/generators.yml index cf2ec186e234..e6b85a8df838 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v31/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v31/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v32/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v32/generators.yml index a4dc0f2df074..76a0106cf46d 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v32/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v32/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: local-file-system path: ../../../src/sdk @@ -17,7 +17,7 @@ groups: sdks: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v33/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v33/generators.yml index eca1a8c0a8c7..952177d4b1b4 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v33/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v33/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com @@ -17,7 +17,7 @@ groups: sdks: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v34/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v34/generators.yml index fd8bf4d100f3..a7b4e114a35b 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v34/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v34/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: local-file-system path: ../../../src/sdk @@ -17,7 +17,7 @@ groups: sdks: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v35/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v35/generators.yml index 2c5ecc4ed322..b83f1b9fe70c 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v35/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v35/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com @@ -17,7 +17,7 @@ groups: sdks: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v36/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v36/generators.yml index ee1571004820..bf35766b03b7 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v36/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v36/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com @@ -17,7 +17,7 @@ groups: sdks: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v37/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v37/generators.yml index b16755830c5c..ee1f2aa44e2f 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v37/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v37/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com @@ -17,7 +17,7 @@ groups: sdks: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v38/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v38/generators.yml index cf5af636a830..54d59b3ab3ff 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v38/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v38/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com @@ -18,7 +18,7 @@ groups: sdks: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v39/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v39/generators.yml index 26bbe9ad5a79..358dae3b65fa 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v39/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v39/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com @@ -18,7 +18,7 @@ groups: sdks: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v40/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v40/generators.yml index ea7b73feede5..199dd3823a70 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v40/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v40/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com @@ -18,7 +18,7 @@ groups: sdks: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v41/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v41/generators.yml index 92e6879947df..b14ab8be91eb 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v41/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v41/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com @@ -18,7 +18,7 @@ groups: sdks: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v42/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v42/generators.yml index 0355186271a3..7c2a6e823727 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v42/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v42/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com @@ -18,7 +18,7 @@ groups: sdks: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v43/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v43/generators.yml index 7aa488982e6e..4d69b28c367e 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v43/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v43/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com @@ -18,7 +18,7 @@ groups: node-sdk: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v44/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v44/generators.yml index 3e41983aa250..f2e9602b05b7 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v44/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v44/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com @@ -18,7 +18,7 @@ groups: node-sdk: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v45/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v45/generators.yml index 33151f6e839d..37ccb8d708d1 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v45/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v45/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com @@ -18,7 +18,7 @@ groups: node-sdk: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v46/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v46/generators.yml index 2f5d6b7c38de..e08d3fd3426a 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v46/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v46/generators.yml @@ -1,8 +1,23 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +default-group: local groups: + local: + generators: + - name: fernapi/fern-typescript-sdk + version: 3.49.0 + output: + location: local-file-system + path: ../../../src/sdk + config: + outputSourceFiles: true + includeUtilsOnUnionMembers: true + noOptionalProperties: true + noSerdeLayer: false + smart-casing: false node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v47/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v47/generators.yml index d6fee1b43823..115446e419bf 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v47/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v47/generators.yml @@ -1,8 +1,23 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +default-group: local groups: + local: + generators: + - name: fernapi/fern-typescript-sdk + version: 3.49.0 + output: + location: local-file-system + path: ../../../src/sdk + config: + outputSourceFiles: true + includeUtilsOnUnionMembers: true + noOptionalProperties: true + noSerdeLayer: false + smart-casing: false node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v48/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v48/generators.yml index 815ed5437588..6840b2dc7f0d 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v48/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v48/generators.yml @@ -1,8 +1,23 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +default-group: local groups: + local: + generators: + - name: fernapi/fern-typescript-sdk + version: 3.49.0 + output: + location: local-file-system + path: ../../../src/sdk + config: + outputSourceFiles: true + includeUtilsOnUnionMembers: true + noOptionalProperties: true + noSerdeLayer: false + smart-casing: false node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v49/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v49/generators.yml index c23bbcda87ce..358235ab2103 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v49/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v49/generators.yml @@ -1,8 +1,23 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +default-group: local groups: + local: + generators: + - name: fernapi/fern-typescript-sdk + version: 3.49.0 + output: + location: local-file-system + path: ../../../src/sdk + config: + outputSourceFiles: true + includeUtilsOnUnionMembers: true + noOptionalProperties: true + noSerdeLayer: false + smart-casing: false node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v50/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v50/generators.yml index 8480010df7c0..5bd9b6dff521 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v50/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v50/generators.yml @@ -1,8 +1,23 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +default-group: local groups: + local: + generators: + - name: fernapi/fern-typescript-sdk + version: 3.49.0 + output: + location: local-file-system + path: ../../../src/sdk + config: + outputSourceFiles: true + includeUtilsOnUnionMembers: true + noOptionalProperties: true + noSerdeLayer: false + smart-casing: false node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v51/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v51/generators.yml index c6377501325c..ed59f42f2c4f 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v51/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v51/generators.yml @@ -1,8 +1,23 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +default-group: local groups: + local: + generators: + - name: fernapi/fern-typescript-sdk + version: 3.49.0 + output: + location: local-file-system + path: ../../../src/sdk + config: + outputSourceFiles: true + includeUtilsOnUnionMembers: true + noOptionalProperties: true + noSerdeLayer: false + smart-casing: false node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v52/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v52/generators.yml index 9e2ef5905b86..523e1abb5825 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v52/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v52/generators.yml @@ -1,8 +1,23 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +default-group: local groups: + local: + generators: + - name: fernapi/fern-typescript-sdk + version: 3.49.0 + output: + location: local-file-system + path: ../../../src/sdk + config: + outputSourceFiles: true + includeUtilsOnUnionMembers: true + noOptionalProperties: true + noSerdeLayer: false + smart-casing: false node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v53/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v53/generators.yml index fc04b91b894c..5568efa5537b 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v53/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v53/generators.yml @@ -1,12 +1,25 @@ # yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json default-group: local groups: + local: + generators: + - name: fernapi/fern-typescript-sdk + version: 3.49.0 + output: + location: local-file-system + path: ../../../src/sdk + config: + outputSourceFiles: true + includeUtilsOnUnionMembers: true + noOptionalProperties: true + noSerdeLayer: false + smart-casing: false dynamic-ir: audiences: - dynamic generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm package-name: "@fern-api/dynamic-ir-sdk" @@ -17,7 +30,7 @@ groups: node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v54/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v54/generators.yml index cb6f33220304..74199f0c7b88 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v54/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v54/generators.yml @@ -1,12 +1,25 @@ # yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json default-group: local groups: + local: + generators: + - name: fernapi/fern-typescript-sdk + version: 3.49.0 + output: + location: local-file-system + path: ../../../src/sdk + config: + outputSourceFiles: true + includeUtilsOnUnionMembers: true + noOptionalProperties: true + noSerdeLayer: false + smart-casing: false dynamic-ir: audiences: - dynamic generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm package-name: "@fern-api/dynamic-ir-sdk" @@ -17,7 +30,7 @@ groups: node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v55/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v55/generators.yml index 4939632ee3ad..a24472a3ad3e 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v55/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v55/generators.yml @@ -1,12 +1,25 @@ # yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json default-group: local groups: + local: + generators: + - name: fernapi/fern-typescript-sdk + version: 3.49.0 + output: + location: local-file-system + path: ../../../src/sdk + config: + outputSourceFiles: true + includeUtilsOnUnionMembers: true + noOptionalProperties: true + noSerdeLayer: false + smart-casing: false dynamic-ir: audiences: - dynamic generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm package-name: "@fern-api/dynamic-ir-sdk" @@ -17,7 +30,7 @@ groups: node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v56/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v56/generators.yml index 89268d398166..27393f3099ee 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v56/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v56/generators.yml @@ -1,12 +1,25 @@ # yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json default-group: local groups: + local: + generators: + - name: fernapi/fern-typescript-sdk + version: 3.49.0 + output: + location: local-file-system + path: ../../../src/sdk + config: + outputSourceFiles: true + includeUtilsOnUnionMembers: true + noOptionalProperties: true + noSerdeLayer: false + smart-casing: false dynamic-ir: audiences: - dynamic generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm package-name: "@fern-api/dynamic-ir-sdk" @@ -17,7 +30,7 @@ groups: node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v57/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v57/generators.yml index 1c6991aa2f74..a805780dd7be 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v57/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v57/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: local-file-system path: ../../../src/sdk @@ -19,7 +19,7 @@ groups: - dynamic generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm package-name: "@fern-api/dynamic-ir-sdk" @@ -30,7 +30,7 @@ groups: node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v58/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v58/generators.yml index 60768e4ec7e8..14469666104a 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v58/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v58/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: local-file-system path: ../../../src/sdk @@ -19,7 +19,7 @@ groups: - dynamic generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm package-name: "@fern-api/dynamic-ir-sdk" @@ -30,7 +30,7 @@ groups: node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v59/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v59/generators.yml index 3b22f47be05d..b1a1bcaeaf9b 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v59/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v59/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: local-file-system path: ../../../src/sdk @@ -19,7 +19,7 @@ groups: - dynamic generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm package-name: "@fern-api/dynamic-ir-sdk" @@ -30,7 +30,7 @@ groups: node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v60/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v60/generators.yml index 4f47228c29aa..42f5fc8bdc98 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v60/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v60/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: local-file-system path: ../../../src/sdk @@ -19,7 +19,7 @@ groups: - dynamic generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm package-name: "@fern-api/dynamic-ir-sdk" @@ -30,7 +30,7 @@ groups: node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v61/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v61/generators.yml index 0ca4be182fc0..58aac206e8da 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v61/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v61/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: local-file-system path: ../../../src/sdk @@ -19,7 +19,7 @@ groups: - dynamic generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm package-name: "@fern-api/dynamic-ir-sdk" @@ -30,7 +30,7 @@ groups: node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v62/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v62/generators.yml index 79657cc37cf5..d886bd6911e9 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v62/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v62/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: local-file-system path: ../../../src/sdk @@ -19,7 +19,7 @@ groups: - dynamic generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm package-name: "@fern-api/dynamic-ir-sdk" @@ -30,7 +30,7 @@ groups: node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v63/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v63/generators.yml index eb3a25a4493f..fc00b642f375 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v63/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v63/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: local-file-system path: ../../../src/sdk @@ -20,7 +20,7 @@ groups: - dynamic generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm package-name: "@fern-api/dynamic-ir-sdk" @@ -32,7 +32,7 @@ groups: node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/src/sdk/core/fetcher/Fetcher.ts b/packages/ir-sdk/src/sdk/core/fetcher/Fetcher.ts index 45cae32b23c1..5e5058a9144f 100644 --- a/packages/ir-sdk/src/sdk/core/fetcher/Fetcher.ts +++ b/packages/ir-sdk/src/sdk/core/fetcher/Fetcher.ts @@ -218,7 +218,13 @@ async function getHeaders(args: Fetcher.Args): Promise { newHeaders.set( "Accept", - args.responseType === "json" ? "application/json" : args.responseType === "text" ? "text/plain" : "*/*", + args.responseType === "json" + ? "application/json" + : args.responseType === "text" + ? "text/plain" + : args.responseType === "sse" + ? "text/event-stream" + : "*/*", ); if (args.body !== undefined && args.contentType != null) { newHeaders.set("Content-Type", args.contentType); diff --git a/packages/ir-sdk/src/sdk/core/schemas/builders/list/list.ts b/packages/ir-sdk/src/sdk/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/packages/ir-sdk/src/sdk/core/schemas/builders/list/list.ts +++ b/packages/ir-sdk/src/sdk/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/ir-sdk/src/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts b/packages/ir-sdk/src/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/packages/ir-sdk/src/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/packages/ir-sdk/src/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/packages/ir-sdk/src/sdk/core/schemas/builders/object/object.ts b/packages/ir-sdk/src/sdk/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/packages/ir-sdk/src/sdk/core/schemas/builders/object/object.ts +++ b/packages/ir-sdk/src/sdk/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/packages/ir-sdk/src/sdk/core/schemas/builders/record/record.ts b/packages/ir-sdk/src/sdk/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/packages/ir-sdk/src/sdk/core/schemas/builders/record/record.ts +++ b/packages/ir-sdk/src/sdk/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/ir-sdk/src/sdk/core/schemas/builders/union/union.ts b/packages/ir-sdk/src/sdk/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/packages/ir-sdk/src/sdk/core/schemas/builders/union/union.ts +++ b/packages/ir-sdk/src/sdk/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/packages/ir-sdk/src/sdk/core/schemas/utils/isPlainObject.ts b/packages/ir-sdk/src/sdk/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/packages/ir-sdk/src/sdk/core/schemas/utils/isPlainObject.ts +++ b/packages/ir-sdk/src/sdk/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/packages/seed/fern/generators.yml b/packages/seed/fern/generators.yml index 3a082c45cef6..65d92c3da2fc 100644 --- a/packages/seed/fern/generators.yml +++ b/packages/seed/fern/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.2 + version: 3.49.0 output: location: local-file-system path: ../src/config diff --git a/packages/seed/src/config/api/resources/config/types/BuildScripts.ts b/packages/seed/src/config/api/resources/config/types/BuildScripts.ts index f44ea4ee0ec7..4e465efbb077 100644 --- a/packages/seed/src/config/api/resources/config/types/BuildScripts.ts +++ b/packages/seed/src/config/api/resources/config/types/BuildScripts.ts @@ -6,8 +6,8 @@ import type * as FernSeedConfig from "../../../index.js"; * Scripts pertaining to the artifact produced by this generator and how to build them (e.g. yarn install && yarn build for the TS SDK) */ export interface BuildScripts { - preInstallScript?: FernSeedConfig.Script; - installScript?: FernSeedConfig.Script; - compileScript?: FernSeedConfig.Script; - testScript?: FernSeedConfig.Script; + preInstallScript?: FernSeedConfig.Script | undefined; + installScript?: FernSeedConfig.Script | undefined; + compileScript?: FernSeedConfig.Script | undefined; + testScript?: FernSeedConfig.Script | undefined; } diff --git a/packages/seed/src/config/api/resources/config/types/CliSeedWorkspaceConfiguration.ts b/packages/seed/src/config/api/resources/config/types/CliSeedWorkspaceConfiguration.ts index 0e9d97d92f84..c515088e2c51 100644 --- a/packages/seed/src/config/api/resources/config/types/CliSeedWorkspaceConfiguration.ts +++ b/packages/seed/src/config/api/resources/config/types/CliSeedWorkspaceConfiguration.ts @@ -7,5 +7,5 @@ export interface CliSeedWorkspaceConfiguration { publishRc: FernSeedConfig.PublishCommand; publishDev: FernSeedConfig.PublishCommand; /** The location of the changelog file, the schema of which must follow FDR's `GeneratorReleaseRequest` object. */ - changelogLocation?: string; + changelogLocation?: string | undefined; } diff --git a/packages/seed/src/config/api/resources/config/types/FixtureConfigurations.ts b/packages/seed/src/config/api/resources/config/types/FixtureConfigurations.ts index a65acb3963c8..3b05154c04ee 100644 --- a/packages/seed/src/config/api/resources/config/types/FixtureConfigurations.ts +++ b/packages/seed/src/config/api/resources/config/types/FixtureConfigurations.ts @@ -3,25 +3,25 @@ import type * as FernSeedConfig from "../../../index.js"; export interface FixtureConfigurations { - publishConfig?: unknown; - publishMetadata?: unknown; - customConfig?: unknown; - readmeConfig?: FernSeedConfig.ReadmeConfig; - audiences?: string[]; + publishConfig?: unknown | undefined; + publishMetadata?: unknown | undefined; + customConfig?: unknown | undefined; + readmeConfig?: FernSeedConfig.ReadmeConfig | undefined; + audiences?: string[] | undefined; outputFolder: string; - outputVersion?: string; + outputVersion?: string | undefined; /** * If true, dynamic snippet tests will not be generated for this fixture. * This is useful for situations where the main parts of the sdk are working, but the dynamic snippet tests are not. * Disabling the dynamic snippet tests allows the fixture to compile and run tests, surfacing issues when failures there start to occur. */ - disableDynamicSnippetTests?: boolean; + disableDynamicSnippetTests?: boolean | undefined; /** Overrides the default output mode */ - outputMode?: FernSeedConfig.OutputMode; - license?: unknown; + outputMode?: FernSeedConfig.OutputMode | undefined; + license?: unknown | undefined; /** * Either a boolean to skip all scripts, or a list of script names to skip. * When true, all build and test scripts will be skipped for this fixture. */ - skipScripts?: FernSeedConfig.SkipScripts; + skipScripts?: FernSeedConfig.SkipScripts | undefined; } diff --git a/packages/seed/src/config/api/resources/config/types/LocalBuildInfo.ts b/packages/seed/src/config/api/resources/config/types/LocalBuildInfo.ts index 8f4640c2e313..dde84f98769c 100644 --- a/packages/seed/src/config/api/resources/config/types/LocalBuildInfo.ts +++ b/packages/seed/src/config/api/resources/config/types/LocalBuildInfo.ts @@ -14,5 +14,5 @@ export interface LocalBuildInfo { */ runCommand: string; /** Environment variables to set when running the generator locally. */ - env?: Record; + env?: Record | undefined; } diff --git a/packages/seed/src/config/api/resources/config/types/PublishCommand.ts b/packages/seed/src/config/api/resources/config/types/PublishCommand.ts index 429f420d4486..4261c25567dd 100644 --- a/packages/seed/src/config/api/resources/config/types/PublishCommand.ts +++ b/packages/seed/src/config/api/resources/config/types/PublishCommand.ts @@ -8,8 +8,8 @@ import type * as FernSeedConfig from "../../../index.js"; * Commands can be multi-line, we'll run them all! */ export interface PublishCommand { - workingDirectory?: string; + workingDirectory?: string | undefined; /** The string to substitute for the version in the command. ex. `"$VERSION"` */ - versionSubstitution?: string; + versionSubstitution?: string | undefined; command: FernSeedConfig.DockerCommand; } diff --git a/packages/seed/src/config/api/resources/config/types/PublishDocker.ts b/packages/seed/src/config/api/resources/config/types/PublishDocker.ts index 69827e6642eb..30f3be98541c 100644 --- a/packages/seed/src/config/api/resources/config/types/PublishDocker.ts +++ b/packages/seed/src/config/api/resources/config/types/PublishDocker.ts @@ -6,7 +6,7 @@ import type * as FernSeedConfig from "../../../index.js"; * Configuration for publishing from a docker image, assuming a vanilla docker deployment. */ export interface PublishDocker { - workingDirectory?: string; - preBuildCommands?: FernSeedConfig.DockerCommand; + workingDirectory?: string | undefined; + preBuildCommands?: FernSeedConfig.DockerCommand | undefined; docker: FernSeedConfig.PublishDockerConfiguration; } diff --git a/packages/seed/src/config/api/resources/config/types/PublishDockerConfiguration.ts b/packages/seed/src/config/api/resources/config/types/PublishDockerConfiguration.ts index 2508c0a67fe6..22af37e4f0a3 100644 --- a/packages/seed/src/config/api/resources/config/types/PublishDockerConfiguration.ts +++ b/packages/seed/src/config/api/resources/config/types/PublishDockerConfiguration.ts @@ -3,6 +3,6 @@ export interface PublishDockerConfiguration { context: string; image: string; - aliases?: string[]; + aliases?: string[] | undefined; file: string; } diff --git a/packages/seed/src/config/api/resources/config/types/ReadmeConfig.ts b/packages/seed/src/config/api/resources/config/types/ReadmeConfig.ts index 24da7db465af..ea7441b439c5 100644 --- a/packages/seed/src/config/api/resources/config/types/ReadmeConfig.ts +++ b/packages/seed/src/config/api/resources/config/types/ReadmeConfig.ts @@ -3,10 +3,10 @@ import type * as FernSeedConfig from "../../../index.js"; export interface ReadmeConfig { - defaultEndpoint?: FernSeedConfig.ReadmeEndpoint; - bannerLink?: string; - apiReferenceLink?: string; - features?: Record; - apiName?: string; - disabledSections?: string[]; + defaultEndpoint?: FernSeedConfig.ReadmeEndpoint | undefined; + bannerLink?: string | undefined; + apiReferenceLink?: string | undefined; + features?: Record | undefined; + apiName?: string | undefined; + disabledSections?: string[] | undefined; } diff --git a/packages/seed/src/config/api/resources/config/types/ReadmeEndpointObject.ts b/packages/seed/src/config/api/resources/config/types/ReadmeEndpointObject.ts index fc59c8b6a37b..9e284ea2ce13 100644 --- a/packages/seed/src/config/api/resources/config/types/ReadmeEndpointObject.ts +++ b/packages/seed/src/config/api/resources/config/types/ReadmeEndpointObject.ts @@ -3,5 +3,5 @@ export interface ReadmeEndpointObject { method: string; path: string; - stream?: boolean; + stream?: boolean | undefined; } diff --git a/packages/seed/src/config/api/resources/config/types/Script.ts b/packages/seed/src/config/api/resources/config/types/Script.ts index 13997f5b0c32..a24293a4ebb9 100644 --- a/packages/seed/src/config/api/resources/config/types/Script.ts +++ b/packages/seed/src/config/api/resources/config/types/Script.ts @@ -4,6 +4,6 @@ import type * as FernSeedConfig from "../../../index.js"; export interface Script { /** The name of the script. If provided, this enables fixtures to skip certain scripts. */ - name?: string; + name?: string | undefined; commands: FernSeedConfig.ScriptCommands; } diff --git a/packages/seed/src/config/api/resources/config/types/ScriptCommandsByPhase.ts b/packages/seed/src/config/api/resources/config/types/ScriptCommandsByPhase.ts index 54fa9e027fd9..05aeac198de4 100644 --- a/packages/seed/src/config/api/resources/config/types/ScriptCommandsByPhase.ts +++ b/packages/seed/src/config/api/resources/config/types/ScriptCommandsByPhase.ts @@ -5,7 +5,7 @@ */ export interface ScriptCommandsByPhase { /** Commands to run during the build phase */ - build?: string[]; + build?: string[] | undefined; /** Commands to run during the test phase */ - test?: string[]; + test?: string[] | undefined; } diff --git a/packages/seed/src/config/api/resources/config/types/SeedWorkspaceConfiguration.ts b/packages/seed/src/config/api/resources/config/types/SeedWorkspaceConfiguration.ts index c0bed138fadb..106e42557b2d 100644 --- a/packages/seed/src/config/api/resources/config/types/SeedWorkspaceConfiguration.ts +++ b/packages/seed/src/config/api/resources/config/types/SeedWorkspaceConfiguration.ts @@ -4,26 +4,26 @@ import type * as FernSeedConfig from "../../../index.js"; export interface SeedWorkspaceConfiguration { image: string; - imageAliases?: string[]; + imageAliases?: string[] | undefined; displayName: string; irVersion: string; test: FernSeedConfig.TestConfiguration; publish: FernSeedConfig.PublishConfiguration; /** The location of the changelog file, the schema of which must follow FDR's `GeneratorReleaseRequest` object. */ - changelogLocation?: string; - language?: FernSeedConfig.Language; - defaultCustomConfig?: Record; + changelogLocation?: string | undefined; + language?: FernSeedConfig.Language | undefined; + defaultCustomConfig?: Record | undefined; defaultOutputMode: FernSeedConfig.OutputMode; generatorType: FernSeedConfig.GeneratorType; - buildScripts?: FernSeedConfig.BuildScripts; + buildScripts?: FernSeedConfig.BuildScripts | undefined; /** Configuration that will be used for any custom fixture specified by --custom-fixture */ - customFixtureConfig?: FernSeedConfig.FixtureConfigurations; - fixtures?: Record; - scripts?: FernSeedConfig.ContainerScriptConfig[]; + customFixtureConfig?: FernSeedConfig.FixtureConfigurations | undefined; + fixtures?: Record | undefined; + scripts?: FernSeedConfig.ContainerScriptConfig[] | undefined; /** * List any fixtures that are okay to fail. For normal fixtures * just list the fixture name. For configured fixture list {fixture}:{outputFolder}. */ - allowedFailures?: string[]; - features?: FernSeedConfig.GeneratorFeatures; + allowedFailures?: string[] | undefined; + features?: FernSeedConfig.GeneratorFeatures | undefined; } diff --git a/packages/seed/src/config/api/resources/config/types/TestConfiguration.ts b/packages/seed/src/config/api/resources/config/types/TestConfiguration.ts index 3683f75c44bc..40a36c956b8f 100644 --- a/packages/seed/src/config/api/resources/config/types/TestConfiguration.ts +++ b/packages/seed/src/config/api/resources/config/types/TestConfiguration.ts @@ -4,6 +4,6 @@ import type * as FernSeedConfig from "../../../index.js"; export interface TestConfiguration { docker: FernSeedConfig.TestContainerConfiguration; - podman?: FernSeedConfig.TestContainerConfiguration; - local?: FernSeedConfig.LocalBuildInfo; + podman?: FernSeedConfig.TestContainerConfiguration | undefined; + local?: FernSeedConfig.LocalBuildInfo | undefined; } diff --git a/packages/seed/src/config/api/resources/config/types/TestContainerConfiguration.ts b/packages/seed/src/config/api/resources/config/types/TestContainerConfiguration.ts index 7f41d8aa4aee..356c49da5ccd 100644 --- a/packages/seed/src/config/api/resources/config/types/TestContainerConfiguration.ts +++ b/packages/seed/src/config/api/resources/config/types/TestContainerConfiguration.ts @@ -4,5 +4,5 @@ import type * as FernSeedConfig from "../../../index.js"; export interface TestContainerConfiguration { image: string; - command?: FernSeedConfig.DockerCommand; + command?: FernSeedConfig.DockerCommand | undefined; } diff --git a/packages/seed/src/config/api/resources/features/types/FeatureImplementationWithDetails.ts b/packages/seed/src/config/api/resources/features/types/FeatureImplementationWithDetails.ts index b214df5ab788..33b29316dbb9 100644 --- a/packages/seed/src/config/api/resources/features/types/FeatureImplementationWithDetails.ts +++ b/packages/seed/src/config/api/resources/features/types/FeatureImplementationWithDetails.ts @@ -7,5 +7,5 @@ export interface FeatureImplementationWithDetails { * Any useful information about the implementation status, if it's begun, if it's behind * a feature flag, etc. */ - details?: string; + details?: string | undefined; } diff --git a/packages/seed/src/config/core/fetcher/Fetcher.ts b/packages/seed/src/config/core/fetcher/Fetcher.ts index 45cae32b23c1..5e5058a9144f 100644 --- a/packages/seed/src/config/core/fetcher/Fetcher.ts +++ b/packages/seed/src/config/core/fetcher/Fetcher.ts @@ -218,7 +218,13 @@ async function getHeaders(args: Fetcher.Args): Promise { newHeaders.set( "Accept", - args.responseType === "json" ? "application/json" : args.responseType === "text" ? "text/plain" : "*/*", + args.responseType === "json" + ? "application/json" + : args.responseType === "text" + ? "text/plain" + : args.responseType === "sse" + ? "text/event-stream" + : "*/*", ); if (args.body !== undefined && args.contentType != null) { newHeaders.set("Content-Type", args.contentType); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a0a0f41725a..2ec6bc29c400 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -103,137 +103,140 @@ catalogs: specifier: 0.0.22 version: 0.0.22 '@fern-fern/ir-v23-sdk': - specifier: 0.0.1 - version: 0.0.1 + specifier: 1.0.0 + version: 1.0.0 '@fern-fern/ir-v24-sdk': - specifier: 0.0.1 - version: 0.0.1 + specifier: 1.0.0 + version: 1.0.0 '@fern-fern/ir-v25-sdk': - specifier: 0.0.1 - version: 0.0.1 + specifier: 1.0.0 + version: 1.0.0 '@fern-fern/ir-v26-sdk': - specifier: 0.0.1 - version: 0.0.1 + specifier: 1.0.0 + version: 1.0.0 '@fern-fern/ir-v27-sdk': - specifier: 0.0.1 - version: 0.0.1 + specifier: 1.0.0 + version: 1.0.0 '@fern-fern/ir-v28-sdk': - specifier: 0.0.3 - version: 0.0.3 + specifier: 1.0.0 + version: 1.0.0 '@fern-fern/ir-v29-sdk': - specifier: 0.0.6 - version: 0.0.6 + specifier: 1.0.0 + version: 1.0.0 '@fern-fern/ir-v3-model': specifier: 0.0.4 version: 0.0.4 '@fern-fern/ir-v30-sdk': - specifier: 0.0.1 - version: 0.0.1 - '@fern-fern/ir-v31-sdk': - specifier: 0.0.5 - version: 0.0.5 - '@fern-fern/ir-v32-sdk': specifier: 1.0.0 version: 1.0.0 - '@fern-fern/ir-v33-sdk': + '@fern-fern/ir-v31-sdk': specifier: 1.0.0 version: 1.0.0 + '@fern-fern/ir-v32-sdk': + specifier: 1.0.1 + version: 1.0.1 + '@fern-fern/ir-v33-sdk': + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v34-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v35-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v36-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v37-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v38-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v39-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v4-model': specifier: 0.0.4 version: 0.0.4 '@fern-fern/ir-v40-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v41-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v42-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v43-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v44-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v45-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v46-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v47-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v48-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v49-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v5-model': specifier: 0.0.3 version: 0.0.3 '@fern-fern/ir-v50-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v51-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v52-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v53-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v54-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v55-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v56-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v57-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v58-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 + '@fern-fern/ir-v59-sdk': + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v6-model': specifier: 0.0.33 version: 0.0.33 '@fern-fern/ir-v60-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v61-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v62-sdk': - specifier: 1.0.0 - version: 1.0.0 - '@fern-fern/ir-v63-sdk': specifier: 1.0.1 version: 1.0.1 + '@fern-fern/ir-v63-sdk': + specifier: 1.0.2 + version: 1.0.2 '@fern-fern/ir-v7-model': specifier: 0.0.2 version: 0.0.2 @@ -6251,139 +6254,139 @@ importers: version: 0.0.22 '@fern-fern/ir-v23-sdk': specifier: 'catalog:' - version: 0.0.1 + version: 1.0.0 '@fern-fern/ir-v24-sdk': specifier: 'catalog:' - version: 0.0.1 + version: 1.0.0 '@fern-fern/ir-v25-sdk': specifier: 'catalog:' - version: 0.0.1 + version: 1.0.0 '@fern-fern/ir-v26-sdk': specifier: 'catalog:' - version: 0.0.1 + version: 1.0.0 '@fern-fern/ir-v27-sdk': specifier: 'catalog:' - version: 0.0.1 + version: 1.0.0 '@fern-fern/ir-v28-sdk': specifier: 'catalog:' - version: 0.0.3 + version: 1.0.0 '@fern-fern/ir-v29-sdk': specifier: 'catalog:' - version: 0.0.6 + version: 1.0.0 '@fern-fern/ir-v3-model': specifier: 'catalog:' version: 0.0.4 '@fern-fern/ir-v30-sdk': specifier: 'catalog:' - version: 0.0.1 + version: 1.0.0 '@fern-fern/ir-v31-sdk': specifier: 'catalog:' - version: 0.0.5 + version: 1.0.0 '@fern-fern/ir-v32-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v33-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v34-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v35-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v36-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v37-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v38-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v39-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v4-model': specifier: 'catalog:' version: 0.0.4 '@fern-fern/ir-v40-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v41-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v42-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v43-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v44-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v45-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v46-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v47-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v48-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v49-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v5-model': specifier: 'catalog:' version: 0.0.3 '@fern-fern/ir-v50-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v51-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v52-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v53-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v54-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v55-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v56-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v57-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v58-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v59-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 'catalog:' + version: 1.0.1 '@fern-fern/ir-v6-model': specifier: 'catalog:' version: 0.0.33 '@fern-fern/ir-v60-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v61-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v62-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v63-sdk': specifier: 'catalog:' - version: 1.0.1 + version: 1.0.2 '@fern-fern/ir-v7-model': specifier: 'catalog:' version: 0.0.2 @@ -9729,174 +9732,183 @@ packages: '@fern-fern/ir-v22-model@0.0.22': resolution: {integrity: sha512-5u+RnhNKfH/0lYdZCRvx4b8waL8FiaVqwB9L4otRfH/nR6Wjb8006KR35+LQNrhlzkrqgXd7NJ7/0Aedv+1Bbw==} - '@fern-fern/ir-v23-sdk@0.0.1': - resolution: {integrity: sha512-Hjvq2PxCWR/5tQUvLdb47/RFUbPwAHLiq+GslWN+DdAoCnY0GwQt5dnxD9Izv+JhM7w4XbxgPPSPnDc1x8kH7w==} + '@fern-fern/ir-v23-sdk@1.0.0': + resolution: {integrity: sha512-BQu7cnRQzotj5wH+1GcmdgA5BgOptnEAqnYHJIwn1UR371a66nk2U1gVmtlpQ3JOEv6+cHyJCIKoJJz5Fr62hQ==} + engines: {node: '>=18.0.0'} - '@fern-fern/ir-v24-sdk@0.0.1': - resolution: {integrity: sha512-FgqOUTNIHh1uuZ7PaYyziCGiS8qhUAhtGv6FsoTposOVd/CziYgY3OnUHzXId5TO9HyfOb3ycYqBWeSh7R/qAQ==} + '@fern-fern/ir-v24-sdk@1.0.0': + resolution: {integrity: sha512-aQRvOAurqyPNLxzVK98o5YtOzF+FTPqZvvsXAC+G1wHviVFKoQS6x0upP3ashzgEBNlT3AMMRbk9HzqD8kr8sA==} + engines: {node: '>=18.0.0'} - '@fern-fern/ir-v25-sdk@0.0.1': - resolution: {integrity: sha512-Xw4tXo0ChAhmgV7gCFcm1eY5cnPZ7Sf9nlIsKyXTQNB0EJSoKqD/zonhu5CVNxCO2rn8GBggyt1LEYFl2fC85w==} + '@fern-fern/ir-v25-sdk@1.0.0': + resolution: {integrity: sha512-XeX37JvdfJ+7j28TrVX6PLiC8RmCkvnDsHIDsS49D79rrB2F+YzLmGREMUUbYX3oxJ38OF11j7zMjI+K+yLgWA==} + engines: {node: '>=18.0.0'} - '@fern-fern/ir-v26-sdk@0.0.1': - resolution: {integrity: sha512-rWWNvMxrPXOwqtbu2FQoWOM+Tegr4fDDU23bVYa+Js8aeUqOwxCfjo9PQReg3qNEHG5IAvvNqXDh7fiWGSgapw==} + '@fern-fern/ir-v26-sdk@1.0.0': + resolution: {integrity: sha512-yLCnKxquHtbnYEytvoYJrhT/KhVKVupS56mzdL+u0LtDbNoJYjh7Coqm7hoZXxPrBKfP5nZN9usGLZVwy8JwHA==} + engines: {node: '>=18.0.0'} - '@fern-fern/ir-v27-sdk@0.0.1': - resolution: {integrity: sha512-cmqSy0+WhNzvQvihnMnKA1Nz8V60vYKSowB1yRU+qFeFkBVV9o1TQ35CUPKJxlZsxwgJJ/yuMHbRIlrmMqzlOQ==} + '@fern-fern/ir-v27-sdk@1.0.0': + resolution: {integrity: sha512-mf3jsbEY7wrEPai84ZgQ/4u54JINqmW+uTXVW3z1NYbwPK/omJcxDox4ygKlzFjaa4rG858BJ6r/HUttMEbsMA==} + engines: {node: '>=18.0.0'} - '@fern-fern/ir-v28-sdk@0.0.3': - resolution: {integrity: sha512-ihKA4Qtdx1co/mY2u3JABzCBac7kMX/qAYTM095lB8s2kFO3BeKkpnK+2uke+SMX2TihuJxzoDIUmhNS9ORhBA==} + '@fern-fern/ir-v28-sdk@1.0.0': + resolution: {integrity: sha512-9GvFcna8dMvSS+lc7X49Ez6EHjU8VSEjMISdgemKyJcFLyqOdYxotR+ZEMLv3OttF3hIcamgYiyTtvVbc1cbdQ==} + engines: {node: '>=18.0.0'} - '@fern-fern/ir-v29-sdk@0.0.6': - resolution: {integrity: sha512-VpkdJKQES0mtVBIwHDSmlfN5emq9RYK9mOH2mzwBYnLqlAK2E7eDHGf0le9SmYAzVfvg1ot/vXYWztOzriLI7g==} + '@fern-fern/ir-v29-sdk@1.0.0': + resolution: {integrity: sha512-zpsbNQxIou5PuCSws8kXWbzNr0Np2SDIAXogi+leIleiGA/1woXYUpDEx9H4sd1gKVFgZWEe3sQvl2bwRysm7Q==} + engines: {node: '>=18.0.0'} '@fern-fern/ir-v3-model@0.0.4': resolution: {integrity: sha512-K29rw+4l+LLTRaLN6D8GOOWVxTFgbYyuYdbp0h63wl5vYR/oaNynNRDSIbBAufbFy23IE/drM7drk5oBGWfU2A==} - '@fern-fern/ir-v30-sdk@0.0.1': - resolution: {integrity: sha512-/63kSdMznK3a1xn/JczwQcQ4bhISg7S+BrD+RVaYHm11OA9CtQf4+RHt6rq4ONdC8dup5Tn0/UZ2B/fkpMpz/A==} + '@fern-fern/ir-v30-sdk@1.0.0': + resolution: {integrity: sha512-6LsT9FaUPYW3I767lrjZufvbUa7eAwmiSm5JOVlH1rwHorbuq22D/hdCUl69o6mDW5V6RCjvfzDwiYOPpJLMxA==} + engines: {node: '>=18.0.0'} - '@fern-fern/ir-v31-sdk@0.0.5': - resolution: {integrity: sha512-oUhaRB25e6daxlmWqhGTkVyslIaXRdYUKrFdBeA3lii0ORUvqEb3qGfJ19oSVlDSpwAawdSwCNtiqA4FVMshpg==} + '@fern-fern/ir-v31-sdk@1.0.0': + resolution: {integrity: sha512-+VZIHJ1bjBNr2tqPEuPv2llUox3ersgxWaujqOOMom+7hOl/+VGnvLSpLfCZtsWvW+9x+gXhfwBiqFtCu2jueA==} + engines: {node: '>=18.0.0'} - '@fern-fern/ir-v32-sdk@1.0.0': - resolution: {integrity: sha512-FsNZenfylr3N/hb0wqZKMtxOwlFWkLvUxPFH1BDEKlki8qr+XEfE1nfUIfCW/gaUwKkMPoOsxkLmaT/Xmn6+jA==} + '@fern-fern/ir-v32-sdk@1.0.1': + resolution: {integrity: sha512-g6n/sXxeObFKGbm7xCv14/KnXKzQDP0ZQFCM4CwLiraMkL328nRPJqheD9bT7tqB4UtdqLFbJdEh2C3xdmQi9A==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v33-sdk@1.0.0': - resolution: {integrity: sha512-6w2VjLFNIeXKmvBT7+HEpXSFtdSL6eNtIU3/uN61Rvmb0u3s6nQGVjhFmpmsqDMTB3Pb2ugvWGe++bp71D3C2g==} + '@fern-fern/ir-v33-sdk@1.0.1': + resolution: {integrity: sha512-ffvjja5um8ZXLCRY/KKTMiPDNMsxAC0IGSQw5zpfsGY1FJZDmFIJw65CN+EEgcqMIeX9MVNYiuNC3P150YTNmA==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v34-sdk@1.0.0': - resolution: {integrity: sha512-jUoVJRCz11NRPjzdxL/EVaGEFW1auaNtpmqia+O+sE0Hx3F2Amss409ZQWAKQaOD1St1c3vsWmPvzIJnfhz/fg==} + '@fern-fern/ir-v34-sdk@1.0.1': + resolution: {integrity: sha512-vTyfSHpO8Ngd1V4ASf0drOUZu0VIR3ulHIC0bveoHE3S9O+5QVssBXs0LmL6dbdCWMwBic5/L8w0PEibM1GnVA==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v35-sdk@1.0.0': - resolution: {integrity: sha512-Xc2c+/W/UGiCEERCfD5eouWAASy+/TbvFH4SYwB3tbsSpWrHUtfdaprZXEywbos07eShfllnIW+GcAxnMVMHgQ==} + '@fern-fern/ir-v35-sdk@1.0.1': + resolution: {integrity: sha512-Zm3acSCjSrY8QyJm1ywk0qFaaLQ+jmIdrI4Z3KiwTyNRH3jEmr516+pG8zr4PO+HNZMm7Q+9qnjEu2MNQvPegQ==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v36-sdk@1.0.0': - resolution: {integrity: sha512-5URqnApLQK6s26cYBhMPfAqXhv6NApnxUkpTur2BdriqwNSqPeB8c59dZoLiZkgKEhSrdLuJsoFiWlSgxEfPsw==} + '@fern-fern/ir-v36-sdk@1.0.1': + resolution: {integrity: sha512-KYla6CLrQgS1lTvRHVp7vuGgP9lZ1fcohpXHtoqCIin7zOlrAFYRfqRESVJa0afk3T2XWNNfTGHQXV10vslspA==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v37-sdk@1.0.0': - resolution: {integrity: sha512-YQDT7NUAtKG+WCj7u0S0mTQ+lGV4752iaQFG9UY4d0xOjJK1OsKKut9tWIZUS6/ky5CJLGzyQU4Iig7Bn20Hng==} + '@fern-fern/ir-v37-sdk@1.0.1': + resolution: {integrity: sha512-1UjVsHeQbaVGmwvhK/OqCF3ZLipBbbeaFIVRa97+0fIN0hrWs0Q/WsxoXkft9E2X5hmJ4WbryrtrlBehyI2bcw==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v38-sdk@1.0.0': - resolution: {integrity: sha512-TncPxyFnB5hy4cnyWsloFYY+xNWafZUrZwW2MDVG95BGJNxnIcxlq+OTvDJ0Nt2adm5TRfoOzt43fLyKYtkb0A==} + '@fern-fern/ir-v38-sdk@1.0.1': + resolution: {integrity: sha512-folDF8KXTpdhpdyeiW/6Sh72LXcf6N+HEEXsDwBffdjbq97CDo4dTXu8D3qtKH9k2irhBHPomet0WtVkODe/kg==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v39-sdk@1.0.0': - resolution: {integrity: sha512-cMEnZM0nbX588PjRna/B2oljKUwEDenDMi7/vsJmM/U6E48t3b9PsN2bibS/52iWbTusgOB9QfIlnzGW2VcjMA==} + '@fern-fern/ir-v39-sdk@1.0.1': + resolution: {integrity: sha512-E5MLsdq6wn7IQjxYrmTXDIUeSAfWfIoicVsIEefQsl4rB298YDvoGZd0t2MNlI1J70cLDql5ENmAP7i4TVld5Q==} engines: {node: '>=18.0.0'} '@fern-fern/ir-v4-model@0.0.4': resolution: {integrity: sha512-Xc3VO//QbgITVm6FCPUe4mTfNeTdP8t+zC4a0F0fBz/5iL8rhRhYbdrWvfafJdrF2Hi+LtwvbAFAUPKKLqH1ZQ==} - '@fern-fern/ir-v40-sdk@1.0.0': - resolution: {integrity: sha512-XVp0sVikvICy2lI7IFq98AyAAxBkcOgEZmKgiaa1lSIVkxRI/Q1CNmW5USRi8BrOII4P2fGS3BKNRkUt9PQFVA==} + '@fern-fern/ir-v40-sdk@1.0.1': + resolution: {integrity: sha512-0UBVDKoBVG4NO7Zc04Jo1lJfZ0gNu7OP1LrIor1F5/eeYRZUSpFJZFFmtS8UFe73jMlV5LEu5hYDt5l/LozPRQ==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v41-sdk@1.0.0': - resolution: {integrity: sha512-RSVZR/1F2DO0QKLo2dTDpyT0FG+4Y1PSGWzeq7mBYqENmUHepr0KLGkdIFP/2GZSAEzklB4+g9e5vVzX2uiNaQ==} + '@fern-fern/ir-v41-sdk@1.0.1': + resolution: {integrity: sha512-3FcOMDdwDVWFgTAaWgn3iHX5r055Iq5Tk2vOwtUl2IhgXWjc+aTjPbriq/sXH0+OcEME0gggeIa7C2t/myGi2Q==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v42-sdk@1.0.0': - resolution: {integrity: sha512-ncPds3TV4x2slchY8finenHeB6ObwLc8dNVwm2N9ZiHw0fGwrO7V+AgK9yI87DXBirDsfXgs+4HrMymsnz0C6w==} + '@fern-fern/ir-v42-sdk@1.0.1': + resolution: {integrity: sha512-qxouEI9pjJD8QadgXkakdpoBD/gOiNtQAkE7MuYk/klvTPd5iuSSANkNjdPXadsI4vqESItUQrpNMFD+CcaKtg==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v43-sdk@1.0.0': - resolution: {integrity: sha512-0wBV5Wc5lgX3go47lEINcpHNQyTzBzLNr4/s6TfOMSF+tXiegrzrB6ZmXtZOibrMFiMqfrwxYTDCoeWx+oEpig==} + '@fern-fern/ir-v43-sdk@1.0.1': + resolution: {integrity: sha512-DzuHF+duzjlCibZlMYxqOBjb6dl2FVLC8AtZ65RN9u3gZtk4EPm0Em6SfeAORmy5pS0P+zyQdH/SJysoqbACTA==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v44-sdk@1.0.0': - resolution: {integrity: sha512-W+o9yCQhZcRDIaskgkHIHq7ziaJXuSPibrJpnqLf+HUB9Azgra0hc9S1rb8gN2g73/bheI5y/Yg4zBreOkU2ow==} + '@fern-fern/ir-v44-sdk@1.0.1': + resolution: {integrity: sha512-e7adqM2Fb1AFvNEU/pxEgDzuU1iQdfv6LiTBby+xOHaYRcT1WGDZKMSmqFH3BSadEKwTB7RIJMtF76+kkYXBHA==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v45-sdk@1.0.0': - resolution: {integrity: sha512-IjEMZJWQHmRYvQbss7+BmaVzEe1MO8gxkhvjf1pBdgFGAtAD/NEb6Ls/MfWV6cVjf3xzri04C12VO8sswOq4fg==} + '@fern-fern/ir-v45-sdk@1.0.1': + resolution: {integrity: sha512-zivY4kDwk5+dj7w3NF2NVF8Tofm3TJOxcOoL7lL3TfFr8dkHovmWUnrx4OBQjhZjrUo5gG3sQx4LYGiE3NDq3g==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v46-sdk@1.0.0': - resolution: {integrity: sha512-cyIdSpuLCn2orWVfiyA5b0duL3znSdvU8vEStpGwj2bDVvwRmQu0avzmPOb/GZpFpYYQ1zkNvE3+kEYAkN27Tg==} + '@fern-fern/ir-v46-sdk@1.0.1': + resolution: {integrity: sha512-BMueJfgREAhUvlxu5U6INJTqx5I5By8Yxk8lupToRZ4k5rViylM/WbzqXlGHGQdBi0tO6XQ4a71/XlazBW+4EA==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v47-sdk@1.0.0': - resolution: {integrity: sha512-XKKNu/aJd/8HNYaUCZp4HwF5NusTsKHBoU7BRRaukhGrEnj++1fzzvCWPhoil0/chKTHo9H/4HLuq44bINFVbw==} + '@fern-fern/ir-v47-sdk@1.0.1': + resolution: {integrity: sha512-AMClUqorMvPdCO0kpire5UYcS4ShgPgTOMZ+Gmgk+tqW0qL7HIskcjsKueqlIsgxYEee4ew26kCnmx1slEvcJQ==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v48-sdk@1.0.0': - resolution: {integrity: sha512-MMkt5UVgzmmQ37J+Ipc+zxgCyPiLUP12CTNDTylZSjzEdhSiYh7/VDPLqou+IyXCyb8iiStksLricSZdshm//w==} + '@fern-fern/ir-v48-sdk@1.0.1': + resolution: {integrity: sha512-GpmGHPt4eF4WAe5npXIjNvrNbqKf65b2RRII5pygm80hRe5ZAByHkBO3LJKTHRzUilKbo+oUjHl/OQqpi/JR5A==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v49-sdk@1.0.0': - resolution: {integrity: sha512-gu4SS61XEGtXZArQ9soJ1QtKMwHTjHH8/nPyxysK/sNzVZDca2KJzDAuZajie7uulXLlm091CuI+0hoLuVDKsQ==} + '@fern-fern/ir-v49-sdk@1.0.1': + resolution: {integrity: sha512-tpZdV7n8FcnEJxWy4DzplpBe9q97/7MVVw0plDnp+ONs8gxDB+k7y3jUqWTLl+/uh1ZusfThGMOLaMgK7Lj70Q==} engines: {node: '>=18.0.0'} '@fern-fern/ir-v5-model@0.0.3': resolution: {integrity: sha512-X3Lk1p5w+O/isbJ/UdIVo9JCE7jyj8B20l+ffsArqmxPgHwKpFCTVTjHZQVLDEAfQXDDfj89PfjQg5N8TN+aSA==} - '@fern-fern/ir-v50-sdk@1.0.0': - resolution: {integrity: sha512-NfW+JKhhBA+b6rR6P7cng/cjiK9z0/FCt19JLkWK1y3buUYyRcq7jOLt6tU3e63HEb2UUu+HincrnQyod4HP7Q==} + '@fern-fern/ir-v50-sdk@1.0.1': + resolution: {integrity: sha512-7jiGWQ/g9D6DJEIkJ23LM80c4SwEx4bPkIVjSybnMZ2TsMal6zCWEj3yVO/gHTFJba54VHriMY+b+CQKYKsfyQ==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v51-sdk@1.0.0': - resolution: {integrity: sha512-nux8MCK3dWxJZqo6b/9/uuJ1W1Wrg9NnGn2xwyBQV2GrHf4+M3+lNKX+gbqLWTue8VadndpCHX0TBcFbpNyuMw==} + '@fern-fern/ir-v51-sdk@1.0.1': + resolution: {integrity: sha512-g+baTFvfChx6MJqeBETkizwaaNAH9KyGo/F891f4hGOv0tMKtytjDPByEz3VJ3XXe5VZS54z9UAhECJyuhAuGw==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v52-sdk@1.0.0': - resolution: {integrity: sha512-S4OcTM3I75BXv9PTBWi0EXFr/Qj5t6jhUQI41wyFxBt2kfHqqIOuX4fart5ijzlkpP+CWXu9o9OR7lwrmmqWOg==} + '@fern-fern/ir-v52-sdk@1.0.1': + resolution: {integrity: sha512-i4wJd5eZJ6IJUJ6p8MIzWa9vu1P/YJUxbRjmmzENx7ULPM+Ub5oaQf0Q/ckw27Htm9ztmCb5Habmj/hEstHDMQ==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v53-sdk@1.0.0': - resolution: {integrity: sha512-VIsfQjq4kpbBYCe4R0OUVQSYfyUgnVBGkj7mXBDCtpl/3gjAajT0pkhmyLfXxgp2sgnpAd9WWr7eB5wI1jaWFg==} + '@fern-fern/ir-v53-sdk@1.0.1': + resolution: {integrity: sha512-SVNbkGBVgE/PkgurqqdFEYkmYBGNZzD2Qh8wF9nApp1NH2Wipte5zbzngl78snGTk/e9DN6+mYHfgQPzv6Pi4w==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v54-sdk@1.0.0': - resolution: {integrity: sha512-46OJ8j33OGuA6n2YIuFKtG9TxA6IugFN4oDyl5ZV1oWO9jpiTuyf4Vv0JDPg35Z7s28Ih5HTVpMFKYrW9OBdLw==} + '@fern-fern/ir-v54-sdk@1.0.1': + resolution: {integrity: sha512-ZriTIck0NJRBKDkAceqQ73YzLrEbMZFQeBN9ZQDfgz0KriZ2d3T/vgCutv2c02FNOx5K/jPsjfQcugeLuuF31A==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v55-sdk@1.0.0': - resolution: {integrity: sha512-eRXbQOsdWfKRly3TI5AaLmS2dt2ockLY4sM3B1hEhbYx7pezqKMoSDW8o++B3Qa6Ew6CdTyA9zuN1xOPCm9QPg==} + '@fern-fern/ir-v55-sdk@1.0.1': + resolution: {integrity: sha512-4gww+WcIgqSdRZTz5dDUBnC1Nb4C6O3IIhS914KAbmz1KZFNQwF7i/t+AGVqPTFkfagJdOaj1bbeGqZOWXiV+w==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v56-sdk@1.0.0': - resolution: {integrity: sha512-8lqGHr5sL8L/p9EB1YDuDW/makyO1XtIvN3hI2KgwZZZ5ZSblS1Doys6gpn27OzTY+Ad2j5eI2Uk22rXA7CrSw==} + '@fern-fern/ir-v56-sdk@1.0.1': + resolution: {integrity: sha512-0fXq8ISZvfv4Ykt3yHK4HfpTnTGsAvltWvaYBPwIh8nHmF5S9AIcPfMYTqdECrJOwhOhKtPMsrUvjezRAKKGkQ==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v57-sdk@1.0.0': - resolution: {integrity: sha512-GrMtw2L2Ssko5Qd4oBbdk4TcAmgP6Ee8VFOadW6TAeqn1IAPSo63ay4OyyFQikyXePjsd6tGbFbBVMhSc3mbUA==} + '@fern-fern/ir-v57-sdk@1.0.1': + resolution: {integrity: sha512-dW2kYzg1Ve754JeBmgwMhMlk46YSpJPrYN2eXdZ/9RZo476+/dZqrzSn++t56lnc3aOdrg8yFeuSm9B0vQykBQ==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v58-sdk@1.0.0': - resolution: {integrity: sha512-H3XnzFESgDSRF7ept1rbEm3F3gnyc0IxDC5YH03OO0jZt4yT+IS3Lu9hyIBqyXkJWeSby2b7B3w0PtqFBpz6Yw==} + '@fern-fern/ir-v58-sdk@1.0.1': + resolution: {integrity: sha512-U2U1OwrFyviyaTDzRloDzgBw7Ko6TrbqQZqZfavDBl1Y1S20Q1QfZLqJfJcMhYQcntRbXKJeReNjYsSJFaBGsQ==} engines: {node: '>=18.0.0'} '@fern-fern/ir-v59-sdk@0.0.8': resolution: {integrity: sha512-rPk7crlijG0PvgYZ7g2y5EdhSMMohjGJGfGKo0g5BzdBnA4KqD/LHdful62aHuibYFdORjtGy6GHu1nEH5SD3Q==} - '@fern-fern/ir-v59-sdk@1.0.0': - resolution: {integrity: sha512-hcwkt38gu9LF169Q1H9Ss31VaZ9Y2OqOfOZ7qckZhZNphpVaR39cyC3Qryn2JsPgoCd3Hr8kmHgxewysQGY3Wg==} + '@fern-fern/ir-v59-sdk@1.0.1': + resolution: {integrity: sha512-8qEWF6LW+Hr4Ssk8CjnyKcPNnEAKWQYid0sSLHhpsytCHuoywmId3i8/UsCkj2PCYqd+L2zX1xOQ+nATTVxeAQ==} engines: {node: '>=18.0.0'} '@fern-fern/ir-v6-model@0.0.33': resolution: {integrity: sha512-I3KLvdrRzSrh5CcpU0v/Mjn4ywZhzVwhqJC8IDIQiP1yPvsCnjgbTBNwLRG8KdJqHfLOgdt/hfVzjeuFugSlGA==} - '@fern-fern/ir-v60-sdk@1.0.0': - resolution: {integrity: sha512-lNuUjOgouTN+8FWhw2Jijz50ou5sbJnJTH8f3kvlrOtcFJyUxRb7qkK5jz2SSKkOR+dieXV8H61h7ic3l04vWw==} + '@fern-fern/ir-v60-sdk@1.0.1': + resolution: {integrity: sha512-0j4MrX0nR9lU5YVFBZGEkxOKVNH4ScNqCR5Ah4vDHzv1Y0pQJcxus7nz4pwL6gi3WSyLENXqxWumXuoJNtD2LA==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v61-sdk@1.0.0': - resolution: {integrity: sha512-s0Cn2rZMX9Jorg2s7n3DSG6JiPRv8seFsTgCYBUGEAj9lIjNw5Ybi5gYgx926EPjZcUmEUzXvpYxEsETyfRDdg==} + '@fern-fern/ir-v61-sdk@1.0.1': + resolution: {integrity: sha512-Nwpt2gRL2KS7I7e2GOAic7+RmtT66dbsfd4e6vaFl/90daOGY3kjoyugZGEGKkzv3F2f0LAcyyFpDhi0VwfJSg==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v62-sdk@1.0.0': - resolution: {integrity: sha512-DghnyC7wHaRgs6v4vaXMpB/mX5QlAu/2bgcTEBrAAy3rRYbzDomda6r5fK80Am3TqSvDWfRG8QTAU4WBMnLnvg==} + '@fern-fern/ir-v62-sdk@1.0.1': + resolution: {integrity: sha512-9tKmyf5evhBF7DJOD9chAw4YHcfcxwnT0ROFRoZVC0NBroM3O9TV5tzzWEQNhHuyH+X+2KAs7d9vFwG7/rVQ4g==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v63-sdk@1.0.1': - resolution: {integrity: sha512-vxbqe6p4onAIYMq5iQGeQUXThF2rZNQJIVdBS3ZV4UyCOcsqyMBZpsXFniikkXa9dAT1K2U8l5Um8vWcKkMxbA==} + '@fern-fern/ir-v63-sdk@1.0.2': + resolution: {integrity: sha512-M1P2yW/KKSfAWjzW0jffPelONtBt72AwwmsutM0WVio0W8BI6PlLC9Tf9xbgYt8HBeN0IP7Hh6rQfxdXeWLRtw==} engines: {node: '>=18.0.0'} '@fern-fern/ir-v7-model@0.0.2': @@ -16544,83 +16556,83 @@ snapshots: '@fern-fern/ir-v22-model@0.0.22': {} - '@fern-fern/ir-v23-sdk@0.0.1': {} + '@fern-fern/ir-v23-sdk@1.0.0': {} - '@fern-fern/ir-v24-sdk@0.0.1': {} + '@fern-fern/ir-v24-sdk@1.0.0': {} - '@fern-fern/ir-v25-sdk@0.0.1': {} + '@fern-fern/ir-v25-sdk@1.0.0': {} - '@fern-fern/ir-v26-sdk@0.0.1': {} + '@fern-fern/ir-v26-sdk@1.0.0': {} - '@fern-fern/ir-v27-sdk@0.0.1': {} + '@fern-fern/ir-v27-sdk@1.0.0': {} - '@fern-fern/ir-v28-sdk@0.0.3': {} + '@fern-fern/ir-v28-sdk@1.0.0': {} - '@fern-fern/ir-v29-sdk@0.0.6': {} + '@fern-fern/ir-v29-sdk@1.0.0': {} '@fern-fern/ir-v3-model@0.0.4': {} - '@fern-fern/ir-v30-sdk@0.0.1': {} + '@fern-fern/ir-v30-sdk@1.0.0': {} - '@fern-fern/ir-v31-sdk@0.0.5': {} + '@fern-fern/ir-v31-sdk@1.0.0': {} - '@fern-fern/ir-v32-sdk@1.0.0': {} + '@fern-fern/ir-v32-sdk@1.0.1': {} - '@fern-fern/ir-v33-sdk@1.0.0': {} + '@fern-fern/ir-v33-sdk@1.0.1': {} - '@fern-fern/ir-v34-sdk@1.0.0': {} + '@fern-fern/ir-v34-sdk@1.0.1': {} - '@fern-fern/ir-v35-sdk@1.0.0': {} + '@fern-fern/ir-v35-sdk@1.0.1': {} - '@fern-fern/ir-v36-sdk@1.0.0': {} + '@fern-fern/ir-v36-sdk@1.0.1': {} - '@fern-fern/ir-v37-sdk@1.0.0': {} + '@fern-fern/ir-v37-sdk@1.0.1': {} - '@fern-fern/ir-v38-sdk@1.0.0': {} + '@fern-fern/ir-v38-sdk@1.0.1': {} - '@fern-fern/ir-v39-sdk@1.0.0': {} + '@fern-fern/ir-v39-sdk@1.0.1': {} '@fern-fern/ir-v4-model@0.0.4': {} - '@fern-fern/ir-v40-sdk@1.0.0': {} + '@fern-fern/ir-v40-sdk@1.0.1': {} - '@fern-fern/ir-v41-sdk@1.0.0': {} + '@fern-fern/ir-v41-sdk@1.0.1': {} - '@fern-fern/ir-v42-sdk@1.0.0': {} + '@fern-fern/ir-v42-sdk@1.0.1': {} - '@fern-fern/ir-v43-sdk@1.0.0': {} + '@fern-fern/ir-v43-sdk@1.0.1': {} - '@fern-fern/ir-v44-sdk@1.0.0': {} + '@fern-fern/ir-v44-sdk@1.0.1': {} - '@fern-fern/ir-v45-sdk@1.0.0': {} + '@fern-fern/ir-v45-sdk@1.0.1': {} - '@fern-fern/ir-v46-sdk@1.0.0': {} + '@fern-fern/ir-v46-sdk@1.0.1': {} - '@fern-fern/ir-v47-sdk@1.0.0': {} + '@fern-fern/ir-v47-sdk@1.0.1': {} - '@fern-fern/ir-v48-sdk@1.0.0': {} + '@fern-fern/ir-v48-sdk@1.0.1': {} - '@fern-fern/ir-v49-sdk@1.0.0': {} + '@fern-fern/ir-v49-sdk@1.0.1': {} '@fern-fern/ir-v5-model@0.0.3': {} - '@fern-fern/ir-v50-sdk@1.0.0': {} + '@fern-fern/ir-v50-sdk@1.0.1': {} - '@fern-fern/ir-v51-sdk@1.0.0': {} + '@fern-fern/ir-v51-sdk@1.0.1': {} - '@fern-fern/ir-v52-sdk@1.0.0': {} + '@fern-fern/ir-v52-sdk@1.0.1': {} - '@fern-fern/ir-v53-sdk@1.0.0': {} + '@fern-fern/ir-v53-sdk@1.0.1': {} - '@fern-fern/ir-v54-sdk@1.0.0': {} + '@fern-fern/ir-v54-sdk@1.0.1': {} - '@fern-fern/ir-v55-sdk@1.0.0': {} + '@fern-fern/ir-v55-sdk@1.0.1': {} - '@fern-fern/ir-v56-sdk@1.0.0': {} + '@fern-fern/ir-v56-sdk@1.0.1': {} - '@fern-fern/ir-v57-sdk@1.0.0': {} + '@fern-fern/ir-v57-sdk@1.0.1': {} - '@fern-fern/ir-v58-sdk@1.0.0': {} + '@fern-fern/ir-v58-sdk@1.0.1': {} '@fern-fern/ir-v59-sdk@0.0.8': dependencies: @@ -16632,17 +16644,17 @@ snapshots: transitivePeerDependencies: - encoding - '@fern-fern/ir-v59-sdk@1.0.0': {} + '@fern-fern/ir-v59-sdk@1.0.1': {} '@fern-fern/ir-v6-model@0.0.33': {} - '@fern-fern/ir-v60-sdk@1.0.0': {} + '@fern-fern/ir-v60-sdk@1.0.1': {} - '@fern-fern/ir-v61-sdk@1.0.0': {} + '@fern-fern/ir-v61-sdk@1.0.1': {} - '@fern-fern/ir-v62-sdk@1.0.0': {} + '@fern-fern/ir-v62-sdk@1.0.1': {} - '@fern-fern/ir-v63-sdk@1.0.1': {} + '@fern-fern/ir-v63-sdk@1.0.2': {} '@fern-fern/ir-v7-model@0.0.2': {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 942613e57f11..f4661eba8828 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -46,50 +46,51 @@ catalog: "@fern-fern/ir-v20-model": 0.0.2 "@fern-fern/ir-v21-model": 0.0.1 "@fern-fern/ir-v22-model": 0.0.22 - "@fern-fern/ir-v23-sdk": 0.0.1 - "@fern-fern/ir-v24-sdk": 0.0.1 - "@fern-fern/ir-v25-sdk": 0.0.1 - "@fern-fern/ir-v26-sdk": 0.0.1 - "@fern-fern/ir-v27-sdk": 0.0.1 - "@fern-fern/ir-v28-sdk": 0.0.3 - "@fern-fern/ir-v29-sdk": 0.0.6 + "@fern-fern/ir-v23-sdk": 1.0.0 + "@fern-fern/ir-v24-sdk": 1.0.0 + "@fern-fern/ir-v25-sdk": 1.0.0 + "@fern-fern/ir-v26-sdk": 1.0.0 + "@fern-fern/ir-v27-sdk": 1.0.0 + "@fern-fern/ir-v28-sdk": 1.0.0 + "@fern-fern/ir-v29-sdk": 1.0.0 "@fern-fern/ir-v3-model": 0.0.4 - "@fern-fern/ir-v30-sdk": 0.0.1 - "@fern-fern/ir-v31-sdk": 0.0.5 - "@fern-fern/ir-v32-sdk": 1.0.0 - "@fern-fern/ir-v33-sdk": 1.0.0 - "@fern-fern/ir-v34-sdk": 1.0.0 - "@fern-fern/ir-v35-sdk": 1.0.0 - "@fern-fern/ir-v36-sdk": 1.0.0 - "@fern-fern/ir-v37-sdk": 1.0.0 - "@fern-fern/ir-v38-sdk": 1.0.0 - "@fern-fern/ir-v39-sdk": 1.0.0 + "@fern-fern/ir-v30-sdk": 1.0.0 + "@fern-fern/ir-v31-sdk": 1.0.0 + "@fern-fern/ir-v32-sdk": 1.0.1 + "@fern-fern/ir-v33-sdk": 1.0.1 + "@fern-fern/ir-v34-sdk": 1.0.1 + "@fern-fern/ir-v35-sdk": 1.0.1 + "@fern-fern/ir-v36-sdk": 1.0.1 + "@fern-fern/ir-v37-sdk": 1.0.1 + "@fern-fern/ir-v38-sdk": 1.0.1 + "@fern-fern/ir-v39-sdk": 1.0.1 "@fern-fern/ir-v4-model": 0.0.4 - "@fern-fern/ir-v40-sdk": 1.0.0 - "@fern-fern/ir-v41-sdk": 1.0.0 - "@fern-fern/ir-v42-sdk": 1.0.0 - "@fern-fern/ir-v43-sdk": 1.0.0 - "@fern-fern/ir-v44-sdk": 1.0.0 - "@fern-fern/ir-v45-sdk": 1.0.0 - "@fern-fern/ir-v46-sdk": 1.0.0 - "@fern-fern/ir-v47-sdk": 1.0.0 - "@fern-fern/ir-v48-sdk": 1.0.0 - "@fern-fern/ir-v49-sdk": 1.0.0 + "@fern-fern/ir-v40-sdk": 1.0.1 + "@fern-fern/ir-v41-sdk": 1.0.1 + "@fern-fern/ir-v42-sdk": 1.0.1 + "@fern-fern/ir-v43-sdk": 1.0.1 + "@fern-fern/ir-v44-sdk": 1.0.1 + "@fern-fern/ir-v45-sdk": 1.0.1 + "@fern-fern/ir-v46-sdk": 1.0.1 + "@fern-fern/ir-v47-sdk": 1.0.1 + "@fern-fern/ir-v48-sdk": 1.0.1 + "@fern-fern/ir-v49-sdk": 1.0.1 "@fern-fern/ir-v5-model": 0.0.3 - "@fern-fern/ir-v50-sdk": 1.0.0 - "@fern-fern/ir-v51-sdk": 1.0.0 - "@fern-fern/ir-v52-sdk": 1.0.0 - "@fern-fern/ir-v53-sdk": 1.0.0 - "@fern-fern/ir-v54-sdk": 1.0.0 - "@fern-fern/ir-v55-sdk": 1.0.0 - "@fern-fern/ir-v56-sdk": 1.0.0 - "@fern-fern/ir-v57-sdk": 1.0.0 - "@fern-fern/ir-v58-sdk": 1.0.0 + "@fern-fern/ir-v50-sdk": 1.0.1 + "@fern-fern/ir-v51-sdk": 1.0.1 + "@fern-fern/ir-v52-sdk": 1.0.1 + "@fern-fern/ir-v53-sdk": 1.0.1 + "@fern-fern/ir-v54-sdk": 1.0.1 + "@fern-fern/ir-v55-sdk": 1.0.1 + "@fern-fern/ir-v56-sdk": 1.0.1 + "@fern-fern/ir-v57-sdk": 1.0.1 + "@fern-fern/ir-v58-sdk": 1.0.1 + "@fern-fern/ir-v59-sdk": 1.0.1 "@fern-fern/ir-v6-model": 0.0.33 - "@fern-fern/ir-v60-sdk": 1.0.0 - "@fern-fern/ir-v61-sdk": 1.0.0 - "@fern-fern/ir-v62-sdk": 1.0.0 - "@fern-fern/ir-v63-sdk": 1.0.1 + "@fern-fern/ir-v60-sdk": 1.0.1 + "@fern-fern/ir-v61-sdk": 1.0.1 + "@fern-fern/ir-v62-sdk": 1.0.1 + "@fern-fern/ir-v63-sdk": 1.0.2 "@fern-fern/ir-v7-model": 0.0.2 "@fern-fern/ir-v8-model": 0.0.1 "@fern-fern/ir-v9-model": 0.0.1 From 969a26965dc59f33fc9367e06e8ee71b071e8a78 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:28:52 -0500 Subject: [PATCH 09/10] chore(ci): add GitHub Actions turbo cache, DO_NOT_TRACK=1, and improve ETE test resilience (#12689) Co-authored-by: unknown <> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/actions/install/action.yaml | 9 +++++++++ .github/workflows/check-devin-pr-assignee.yml | 3 +++ .github/workflows/ci-dynamic-snippets.yml | 1 + .github/workflows/ci.yml | 15 +++++++++++++-- .github/workflows/cli-release.yml | 1 + .github/workflows/definitions-validation.yml | 1 + .github/workflows/dependabot.yml | 3 +++ .github/workflows/fix-lint.yml | 1 + .github/workflows/go-generator.yml | 3 +++ .github/workflows/ir-release.yml | 3 +++ .github/workflows/ir-validation.yml | 3 +++ .github/workflows/java-generator.yml | 3 +++ .github/workflows/lint-pr-title.yml | 3 +++ .github/workflows/nightly-seed-metrics.yml | 3 +++ .github/workflows/openapi-ir-validation.yml | 3 +++ .github/workflows/publish-cli.yml | 1 + .github/workflows/publish-docs-parsers-sdk.yml | 3 +++ .github/workflows/publish-generator-cli.yml | 1 + .../workflows/publish-generator-csharp-model.yml | 3 +++ .../workflows/publish-generator-csharp-sdk.yml | 3 +++ .github/workflows/publish-generator-go-model.yml | 3 +++ .github/workflows/publish-generator-go-sdk.yml | 3 +++ .../workflows/publish-generator-java-model.yml | 3 +++ .github/workflows/publish-generator-java-sdk.yml | 3 +++ .../workflows/publish-generator-java-spring.yml | 3 +++ .../workflows/publish-generator-migrations.yml | 1 + .github/workflows/publish-generator-openapi.yml | 3 +++ .github/workflows/publish-generator-php-model.yml | 3 +++ .github/workflows/publish-generator-php-sdk.yml | 3 +++ .github/workflows/publish-generator-postman.yml | 3 +++ .../publish-generator-python-fastapi.yml | 3 +++ .../publish-generator-python-pydantic.yml | 3 +++ .../workflows/publish-generator-python-sdk.yml | 3 +++ .github/workflows/publish-generator-ruby-sdk.yml | 3 +++ .../workflows/publish-generator-rust-model.yml | 3 +++ .github/workflows/publish-generator-rust-sdk.yml | 3 +++ .github/workflows/publish-generator-swift-sdk.yml | 3 +++ .../workflows/publish-generator-ts-express.yml | 3 +++ .github/workflows/publish-generator-ts-sdk.yml | 3 +++ .github/workflows/publish-python-sdk.yml | 3 +++ .github/workflows/publish-snippets-core.yml | 1 + .github/workflows/publish-typescript-sdk.yml | 3 +++ .github/workflows/python-generator.yml | 3 +++ .github/workflows/rerun.yml | 3 +++ .github/workflows/sdk-ete-tests.yml | 1 + .../security-scanning-and-remediation.yml | 1 + .github/workflows/seed-dockers.yml | 1 + .github/workflows/seed-pr-comment.yml | 3 +++ .github/workflows/seed.yml | 1 + .github/workflows/stale-bot.yml | 3 +++ .github/workflows/test-definitions.yml | 1 + .../test-remote-vs-local-generation-parity.yml | 1 + .github/workflows/typescript-generator.yml | 3 +++ .github/workflows/update-seed.yml | 1 + .github/workflows/update-test.yml | 3 +++ .github/workflows/vale.yml | 3 +++ .github/workflows/validate-changelog.yml | 1 + .../workflows/validate-ir-version-consistency.yml | 3 +++ .github/workflows/validate-versions-files.yml | 3 +++ .github/workflows/write-changelogs-to-docs.yml | 3 +++ 60 files changed, 164 insertions(+), 2 deletions(-) diff --git a/.github/actions/install/action.yaml b/.github/actions/install/action.yaml index 9f259ac092af..5129d9832fc9 100644 --- a/.github/actions/install/action.yaml +++ b/.github/actions/install/action.yaml @@ -15,6 +15,15 @@ runs: registry-url: "https://registry.npmjs.org" cache: pnpm + - name: ⎔ Cache turbo build + uses: actions/cache@v4 + with: + path: .turbo + key: ${{ runner.os }}-turbo-${{ github.job }}-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-turbo-${{ github.job }}- + ${{ runner.os }}-turbo- + # Ensure npm 11.5.1 or later is installed - name: ⎔ Update npm shell: bash diff --git a/.github/workflows/check-devin-pr-assignee.yml b/.github/workflows/check-devin-pr-assignee.yml index 4feb838ab193..c32041f0079d 100644 --- a/.github/workflows/check-devin-pr-assignee.yml +++ b/.github/workflows/check-devin-pr-assignee.yml @@ -7,6 +7,9 @@ on: permissions: pull-requests: write +env: + DO_NOT_TRACK: "1" + jobs: check-assignee: runs-on: ubuntu-latest diff --git a/.github/workflows/ci-dynamic-snippets.yml b/.github/workflows/ci-dynamic-snippets.yml index b452ff75c7b7..71c5850b6a8c 100644 --- a/.github/workflows/ci-dynamic-snippets.yml +++ b/.github/workflows/ci-dynamic-snippets.yml @@ -16,6 +16,7 @@ permissions: contents: read env: + DO_NOT_TRACK: "1" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" TURBO_REMOTE_CACHE_TIMEOUT: 60 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b975a7cf36a..d1f4d5cc1b21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,7 @@ concurrency: cancel-in-progress: true env: + DO_NOT_TRACK: "1" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" TURBO_REMOTE_CACHE_TIMEOUT: 60 @@ -186,19 +187,29 @@ jobs: - name: Install protoc-gen-openapi run: go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest + - name: Build CLI (dev) + timeout-minutes: 5 + run: pnpm turbo run dist:cli:dev --filter=@fern-api/cli --filter=@fern-api/cli-v2 + + - name: Build seed CLI + timeout-minutes: 5 + run: pnpm seed:build + - name: Run ETE tests if: ${{ !github.event.inputs.update_snapshots }} + timeout-minutes: 10 env: FERN_ORG_TOKEN_DEV: ${{ secrets.FERN_ORG_TOKEN_DEV }} run: | - FERN_TOKEN=${{ secrets.FERN_ORG_TOKEN_DEV }} pnpm test:ete + FERN_TOKEN=${{ secrets.FERN_ORG_TOKEN_DEV }} pnpm --filter @fern-api/ete-tests test - name: Run ETE tests with snapshot updates if: ${{ github.event.inputs.update_snapshots == 'true' }} + timeout-minutes: 10 env: FERN_ORG_TOKEN_DEV: ${{ secrets.FERN_ORG_TOKEN_DEV }} run: | - FERN_TOKEN=${{ secrets.FERN_ORG_TOKEN_DEV }} pnpm test:ete:update + FERN_TOKEN=${{ secrets.FERN_ORG_TOKEN_DEV }} pnpm --filter @fern-api/ete-tests test -- -u - name: Commit and push updated snapshots if: ${{ github.event.inputs.update_snapshots == 'true' }} diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml index fb7388a57bc9..6f72346802a3 100644 --- a/.github/workflows/cli-release.yml +++ b/.github/workflows/cli-release.yml @@ -13,6 +13,7 @@ permissions: pull-requests: write env: + DO_NOT_TRACK: "1" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} jobs: diff --git a/.github/workflows/definitions-validation.yml b/.github/workflows/definitions-validation.yml index a332309b7a2d..2b86df74d206 100644 --- a/.github/workflows/definitions-validation.yml +++ b/.github/workflows/definitions-validation.yml @@ -8,6 +8,7 @@ on: - main env: + DO_NOT_TRACK: "1" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" TURBO_REMOTE_CACHE_TIMEOUT: 60 diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml index 4f4664fc3717..44be0336ff24 100644 --- a/.github/workflows/dependabot.yml +++ b/.github/workflows/dependabot.yml @@ -10,6 +10,9 @@ permissions: contents: write pull-requests: write +env: + DO_NOT_TRACK: "1" + jobs: auto-merge: runs-on: ubuntu-latest diff --git a/.github/workflows/fix-lint.yml b/.github/workflows/fix-lint.yml index de84c6f94bf4..70acb4864c9f 100644 --- a/.github/workflows/fix-lint.yml +++ b/.github/workflows/fix-lint.yml @@ -11,6 +11,7 @@ concurrency: cancel-in-progress: true env: + DO_NOT_TRACK: "1" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" TURBO_REMOTE_CACHE_TIMEOUT: 60 diff --git a/.github/workflows/go-generator.yml b/.github/workflows/go-generator.yml index ea290455421e..5924895f4d8a 100644 --- a/.github/workflows/go-generator.yml +++ b/.github/workflows/go-generator.yml @@ -8,6 +8,9 @@ on: - main workflow_call: +env: + DO_NOT_TRACK: "1" + jobs: compile: runs-on: ubuntu-latest diff --git a/.github/workflows/ir-release.yml b/.github/workflows/ir-release.yml index 03b7d69e73c4..889478331a69 100644 --- a/.github/workflows/ir-release.yml +++ b/.github/workflows/ir-release.yml @@ -8,6 +8,9 @@ on: paths: - "packages/ir-sdk/fern/apis/ir-types-latest/VERSION" +env: + DO_NOT_TRACK: "1" + jobs: node: runs-on: ubuntu-latest diff --git a/.github/workflows/ir-validation.yml b/.github/workflows/ir-validation.yml index 0472ac7a95ac..132734520c7e 100644 --- a/.github/workflows/ir-validation.yml +++ b/.github/workflows/ir-validation.yml @@ -13,6 +13,9 @@ on: branches: - main +env: + DO_NOT_TRACK: "1" + jobs: run: runs-on: ubuntu-latest diff --git a/.github/workflows/java-generator.yml b/.github/workflows/java-generator.yml index 228ba376e34e..5a07634fd576 100644 --- a/.github/workflows/java-generator.yml +++ b/.github/workflows/java-generator.yml @@ -8,6 +8,9 @@ on: - main workflow_call: +env: + DO_NOT_TRACK: "1" + jobs: compile: runs-on: ubuntu-latest diff --git a/.github/workflows/lint-pr-title.yml b/.github/workflows/lint-pr-title.yml index 91de1a979cae..6137f56900bb 100644 --- a/.github/workflows/lint-pr-title.yml +++ b/.github/workflows/lint-pr-title.yml @@ -11,6 +11,9 @@ on: permissions: pull-requests: read +env: + DO_NOT_TRACK: "1" + jobs: run: name: Lint PR title diff --git a/.github/workflows/nightly-seed-metrics.yml b/.github/workflows/nightly-seed-metrics.yml index bdca34341949..57312d54d4a9 100644 --- a/.github/workflows/nightly-seed-metrics.yml +++ b/.github/workflows/nightly-seed-metrics.yml @@ -12,6 +12,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + DO_NOT_TRACK: "1" + jobs: trigger-seed-snapshot-tests: runs-on: ubuntu-latest diff --git a/.github/workflows/openapi-ir-validation.yml b/.github/workflows/openapi-ir-validation.yml index 68b7fc669327..d8ef9a01e330 100644 --- a/.github/workflows/openapi-ir-validation.yml +++ b/.github/workflows/openapi-ir-validation.yml @@ -13,6 +13,9 @@ on: branches: - main +env: + DO_NOT_TRACK: "1" + jobs: run: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index 542c30fb16e8..a38be8367977 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -24,6 +24,7 @@ permissions: id-token: write # Required for OIDC env: + DO_NOT_TRACK: "1" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" TURBO_REMOTE_CACHE_TIMEOUT: 60 diff --git a/.github/workflows/publish-docs-parsers-sdk.yml b/.github/workflows/publish-docs-parsers-sdk.yml index 10f991f4a439..1941a7b62231 100644 --- a/.github/workflows/publish-docs-parsers-sdk.yml +++ b/.github/workflows/publish-docs-parsers-sdk.yml @@ -3,6 +3,9 @@ name: Publish @fern-fern/docs-parsers-fern-definition on: workflow_dispatch: +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-cli.yml b/.github/workflows/publish-generator-cli.yml index 1d32ac95d291..9b862cfcb282 100644 --- a/.github/workflows/publish-generator-cli.yml +++ b/.github/workflows/publish-generator-cli.yml @@ -8,6 +8,7 @@ on: - "packages/generator-cli/versions.yml" env: + DO_NOT_TRACK: "1" PACKAGE_NAME: "@fern-api/generator-cli" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" diff --git a/.github/workflows/publish-generator-csharp-model.yml b/.github/workflows/publish-generator-csharp-model.yml index 06ee69b6db4e..e21453348bb4 100644 --- a/.github/workflows/publish-generator-csharp-model.yml +++ b/.github/workflows/publish-generator-csharp-model.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-csharp-sdk.yml b/.github/workflows/publish-generator-csharp-sdk.yml index 0828c30638a4..57427a0ae99b 100644 --- a/.github/workflows/publish-generator-csharp-sdk.yml +++ b/.github/workflows/publish-generator-csharp-sdk.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-go-model.yml b/.github/workflows/publish-generator-go-model.yml index 96d3369499b7..74ac28bfcc8c 100644 --- a/.github/workflows/publish-generator-go-model.yml +++ b/.github/workflows/publish-generator-go-model.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-go-sdk.yml b/.github/workflows/publish-generator-go-sdk.yml index 94c7d61ced19..43ac0d5b19a6 100644 --- a/.github/workflows/publish-generator-go-sdk.yml +++ b/.github/workflows/publish-generator-go-sdk.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-java-model.yml b/.github/workflows/publish-generator-java-model.yml index 54619a061ac2..d63e58d5a430 100644 --- a/.github/workflows/publish-generator-java-model.yml +++ b/.github/workflows/publish-generator-java-model.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-java-sdk.yml b/.github/workflows/publish-generator-java-sdk.yml index 0bdd4d39fddb..79823aa81317 100644 --- a/.github/workflows/publish-generator-java-sdk.yml +++ b/.github/workflows/publish-generator-java-sdk.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: permissions: diff --git a/.github/workflows/publish-generator-java-spring.yml b/.github/workflows/publish-generator-java-spring.yml index bc64f8ffd2f2..36066856ff68 100644 --- a/.github/workflows/publish-generator-java-spring.yml +++ b/.github/workflows/publish-generator-java-spring.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-migrations.yml b/.github/workflows/publish-generator-migrations.yml index 1221ea2bfb10..8276409395a3 100644 --- a/.github/workflows/publish-generator-migrations.yml +++ b/.github/workflows/publish-generator-migrations.yml @@ -15,6 +15,7 @@ on: type: string env: + DO_NOT_TRACK: "1" PACKAGE_NAME: "@fern-api/generator-migrations" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" diff --git a/.github/workflows/publish-generator-openapi.yml b/.github/workflows/publish-generator-openapi.yml index 23d8eb909078..c038cb930059 100644 --- a/.github/workflows/publish-generator-openapi.yml +++ b/.github/workflows/publish-generator-openapi.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-php-model.yml b/.github/workflows/publish-generator-php-model.yml index e1021340bed7..1394015a2614 100644 --- a/.github/workflows/publish-generator-php-model.yml +++ b/.github/workflows/publish-generator-php-model.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-php-sdk.yml b/.github/workflows/publish-generator-php-sdk.yml index 73f81ed4bae9..b884b99e2426 100644 --- a/.github/workflows/publish-generator-php-sdk.yml +++ b/.github/workflows/publish-generator-php-sdk.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-postman.yml b/.github/workflows/publish-generator-postman.yml index ba18efd01a84..db313e78bdc9 100644 --- a/.github/workflows/publish-generator-postman.yml +++ b/.github/workflows/publish-generator-postman.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-python-fastapi.yml b/.github/workflows/publish-generator-python-fastapi.yml index e17d5056b446..e7f34b197373 100644 --- a/.github/workflows/publish-generator-python-fastapi.yml +++ b/.github/workflows/publish-generator-python-fastapi.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-python-pydantic.yml b/.github/workflows/publish-generator-python-pydantic.yml index 23cd691da28a..48250b63a507 100644 --- a/.github/workflows/publish-generator-python-pydantic.yml +++ b/.github/workflows/publish-generator-python-pydantic.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-python-sdk.yml b/.github/workflows/publish-generator-python-sdk.yml index 27520d73b8c3..f8c4ce650999 100644 --- a/.github/workflows/publish-generator-python-sdk.yml +++ b/.github/workflows/publish-generator-python-sdk.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-ruby-sdk.yml b/.github/workflows/publish-generator-ruby-sdk.yml index 2513530639ad..2ef322466b92 100644 --- a/.github/workflows/publish-generator-ruby-sdk.yml +++ b/.github/workflows/publish-generator-ruby-sdk.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-rust-model.yml b/.github/workflows/publish-generator-rust-model.yml index 73d3ed021ee7..26390dcc70f3 100644 --- a/.github/workflows/publish-generator-rust-model.yml +++ b/.github/workflows/publish-generator-rust-model.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-rust-sdk.yml b/.github/workflows/publish-generator-rust-sdk.yml index ddb83a269698..d87566290c1b 100644 --- a/.github/workflows/publish-generator-rust-sdk.yml +++ b/.github/workflows/publish-generator-rust-sdk.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-swift-sdk.yml b/.github/workflows/publish-generator-swift-sdk.yml index fb653c4f0366..e45eb59224ec 100644 --- a/.github/workflows/publish-generator-swift-sdk.yml +++ b/.github/workflows/publish-generator-swift-sdk.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-ts-express.yml b/.github/workflows/publish-generator-ts-express.yml index 3c5d504819cd..dd68848f56ad 100644 --- a/.github/workflows/publish-generator-ts-express.yml +++ b/.github/workflows/publish-generator-ts-express.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-ts-sdk.yml b/.github/workflows/publish-generator-ts-sdk.yml index c2427474cb3e..a467102064ee 100644 --- a/.github/workflows/publish-generator-ts-sdk.yml +++ b/.github/workflows/publish-generator-ts-sdk.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-python-sdk.yml b/.github/workflows/publish-python-sdk.yml index cdea5c59edd7..5dc4a2c98ba6 100644 --- a/.github/workflows/publish-python-sdk.yml +++ b/.github/workflows/publish-python-sdk.yml @@ -8,6 +8,9 @@ on: required: true type: string +env: + DO_NOT_TRACK: "1" + jobs: release: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-snippets-core.yml b/.github/workflows/publish-snippets-core.yml index fe56188286e6..2d50fcb9da6c 100644 --- a/.github/workflows/publish-snippets-core.yml +++ b/.github/workflows/publish-snippets-core.yml @@ -9,6 +9,7 @@ on: type: string env: + DO_NOT_TRACK: "1" PACKAGE_NAME: "@fern-api/snippets-core" GITHUB_TOKEN: ${{ secrets.FERN_GITHUB_TOKEN }} diff --git a/.github/workflows/publish-typescript-sdk.yml b/.github/workflows/publish-typescript-sdk.yml index 492e5a9da372..0dbbcaeccde0 100644 --- a/.github/workflows/publish-typescript-sdk.yml +++ b/.github/workflows/publish-typescript-sdk.yml @@ -8,6 +8,9 @@ on: required: true type: string +env: + DO_NOT_TRACK: "1" + jobs: release: runs-on: ubuntu-latest diff --git a/.github/workflows/python-generator.yml b/.github/workflows/python-generator.yml index cf22f789f00f..762a25b6748b 100644 --- a/.github/workflows/python-generator.yml +++ b/.github/workflows/python-generator.yml @@ -8,6 +8,9 @@ on: - main workflow_call: +env: + DO_NOT_TRACK: "1" + jobs: compile: runs-on: ubuntu-22.04 diff --git a/.github/workflows/rerun.yml b/.github/workflows/rerun.yml index d9b6f4ccff8e..ad8c256af209 100644 --- a/.github/workflows/rerun.yml +++ b/.github/workflows/rerun.yml @@ -5,6 +5,9 @@ on: inputs: run_id: required: true +env: + DO_NOT_TRACK: "1" + jobs: rerun: runs-on: ubuntu-latest diff --git a/.github/workflows/sdk-ete-tests.yml b/.github/workflows/sdk-ete-tests.yml index 596aeb317913..ee39c90f14f8 100644 --- a/.github/workflows/sdk-ete-tests.yml +++ b/.github/workflows/sdk-ete-tests.yml @@ -145,6 +145,7 @@ on: - 'generators/go/**' env: + DO_NOT_TRACK: "1" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" TURBO_REMOTE_CACHE_TIMEOUT: 60 diff --git a/.github/workflows/security-scanning-and-remediation.yml b/.github/workflows/security-scanning-and-remediation.yml index c8f51bf5b4ed..b96b2624778d 100644 --- a/.github/workflows/security-scanning-and-remediation.yml +++ b/.github/workflows/security-scanning-and-remediation.yml @@ -31,6 +31,7 @@ on: type: boolean env: + DO_NOT_TRACK: "1" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" TURBO_REMOTE_CACHE_TIMEOUT: 60 diff --git a/.github/workflows/seed-dockers.yml b/.github/workflows/seed-dockers.yml index 1048ae4d5ebd..2d3c02d5d89b 100644 --- a/.github/workflows/seed-dockers.yml +++ b/.github/workflows/seed-dockers.yml @@ -40,6 +40,7 @@ concurrency: cancel-in-progress: true env: + DO_NOT_TRACK: "1" DOCKER_BUILDKIT: 1 jobs: diff --git a/.github/workflows/seed-pr-comment.yml b/.github/workflows/seed-pr-comment.yml index ce516f3f0cb9..63c630599bb9 100644 --- a/.github/workflows/seed-pr-comment.yml +++ b/.github/workflows/seed-pr-comment.yml @@ -16,6 +16,9 @@ concurrency: group: seed-pr-comment-${{ github.event.pull_request.number || github.event.issue.number }} cancel-in-progress: true +env: + DO_NOT_TRACK: "1" + jobs: post-seed-selector: name: Post Seed Selector Comment diff --git a/.github/workflows/seed.yml b/.github/workflows/seed.yml index 7bdd6a6815be..1a58d79c9e52 100644 --- a/.github/workflows/seed.yml +++ b/.github/workflows/seed.yml @@ -20,6 +20,7 @@ concurrency: cancel-in-progress: true env: + DO_NOT_TRACK: "1" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" TURBO_REMOTE_CACHE_TIMEOUT: 60 diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml index b129f589cd0e..db94b4c54698 100644 --- a/.github/workflows/stale-bot.yml +++ b/.github/workflows/stale-bot.yml @@ -8,6 +8,9 @@ permissions: pull-requests: write contents: write +env: + DO_NOT_TRACK: "1" + jobs: stale-prs: runs-on: ubuntu-latest diff --git a/.github/workflows/test-definitions.yml b/.github/workflows/test-definitions.yml index 013e0b1bcefb..e6c26e9c7e53 100644 --- a/.github/workflows/test-definitions.yml +++ b/.github/workflows/test-definitions.yml @@ -22,6 +22,7 @@ concurrency: cancel-in-progress: true env: + DO_NOT_TRACK: "1" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" TURBO_REMOTE_CACHE_TIMEOUT: 60 diff --git a/.github/workflows/test-remote-vs-local-generation-parity.yml b/.github/workflows/test-remote-vs-local-generation-parity.yml index cfdf206d9b38..d759df6f1e95 100644 --- a/.github/workflows/test-remote-vs-local-generation-parity.yml +++ b/.github/workflows/test-remote-vs-local-generation-parity.yml @@ -32,6 +32,7 @@ concurrency: cancel-in-progress: false env: + DO_NOT_TRACK: "1" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" TURBO_REMOTE_CACHE_TIMEOUT: 60 diff --git a/.github/workflows/typescript-generator.yml b/.github/workflows/typescript-generator.yml index 6f9e5bdb84a7..0bb38af774d5 100644 --- a/.github/workflows/typescript-generator.yml +++ b/.github/workflows/typescript-generator.yml @@ -8,6 +8,9 @@ on: required: true type: string +env: + DO_NOT_TRACK: "1" + jobs: release: runs-on: ubuntu-latest diff --git a/.github/workflows/update-seed.yml b/.github/workflows/update-seed.yml index 76e253c15e2b..36face96bbfc 100644 --- a/.github/workflows/update-seed.yml +++ b/.github/workflows/update-seed.yml @@ -50,6 +50,7 @@ permissions: contents: write env: + DO_NOT_TRACK: "1" DOCKER_BUILDKIT: 1 TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" diff --git a/.github/workflows/update-test.yml b/.github/workflows/update-test.yml index c4d0bf3e403c..80191a1439c1 100644 --- a/.github/workflows/update-test.yml +++ b/.github/workflows/update-test.yml @@ -11,6 +11,9 @@ concurrency: permissions: contents: write +env: + DO_NOT_TRACK: "1" + jobs: update-test: if: github.repository == 'fern-api/fern' && github.ref != 'refs/heads/main' diff --git a/.github/workflows/vale.yml b/.github/workflows/vale.yml index cf54337ed010..5c915fc6922e 100644 --- a/.github/workflows/vale.yml +++ b/.github/workflows/vale.yml @@ -4,6 +4,9 @@ on: paths: - "fern/docs/pages/**" +env: + DO_NOT_TRACK: "1" + jobs: vale: name: runner / vale diff --git a/.github/workflows/validate-changelog.yml b/.github/workflows/validate-changelog.yml index 5d6b814a9728..5e21aa0b0d1d 100644 --- a/.github/workflows/validate-changelog.yml +++ b/.github/workflows/validate-changelog.yml @@ -23,6 +23,7 @@ concurrency: cancel-in-progress: true env: + DO_NOT_TRACK: "1" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" TURBO_REMOTE_CACHE_TIMEOUT: 60 diff --git a/.github/workflows/validate-ir-version-consistency.yml b/.github/workflows/validate-ir-version-consistency.yml index eceab5c7507f..ee3331b7e5fe 100644 --- a/.github/workflows/validate-ir-version-consistency.yml +++ b/.github/workflows/validate-ir-version-consistency.yml @@ -22,6 +22,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +env: + DO_NOT_TRACK: "1" + jobs: validate-ir-versions: name: Validate IR Version Consistency diff --git a/.github/workflows/validate-versions-files.yml b/.github/workflows/validate-versions-files.yml index f9ba56c16503..e2ca2665172a 100644 --- a/.github/workflows/validate-versions-files.yml +++ b/.github/workflows/validate-versions-files.yml @@ -4,6 +4,9 @@ on: pull_request: workflow_dispatch: +env: + DO_NOT_TRACK: "1" + jobs: validate: name: Validate versions.yml files diff --git a/.github/workflows/write-changelogs-to-docs.yml b/.github/workflows/write-changelogs-to-docs.yml index 39114c37792f..921906bb2761 100644 --- a/.github/workflows/write-changelogs-to-docs.yml +++ b/.github/workflows/write-changelogs-to-docs.yml @@ -10,6 +10,9 @@ permissions: pull-requests: write contents: write +env: + DO_NOT_TRACK: "1" + jobs: copy-changelogs: runs-on: ubuntu-latest From 9a6c8a705a7cfa90b03ec518185dabf7f93670ea Mon Sep 17 00:00:00 2001 From: Fern Support <126544928+fern-support@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:58:46 -0500 Subject: [PATCH 10/10] fix: Devin reported bug for workspace validation caching (#12695) Co-authored-by: unknown <> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/utils/validateStructureOfYamlFiles.ts | 53 ++++++++++++------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/packages/cli/workspace/lazy-fern-workspace/src/utils/validateStructureOfYamlFiles.ts b/packages/cli/workspace/lazy-fern-workspace/src/utils/validateStructureOfYamlFiles.ts index 371e3befb9db..f8948f92ff75 100644 --- a/packages/cli/workspace/lazy-fern-workspace/src/utils/validateStructureOfYamlFiles.ts +++ b/packages/cli/workspace/lazy-fern-workspace/src/utils/validateStructureOfYamlFiles.ts @@ -20,6 +20,16 @@ function asJsonSchema(schema: Record): Parameters[1]; } +/** + * A cached parse result that tracks whether JSON schema validation was performed. + * Entries cached via skipValidation are only returned to callers that also skip + * validation, ensuring that callers requesting full validation always get it. + */ +interface CachedEntry { + value: T; + validated: boolean; +} + /** * Module-level caches for validated/parsed file contents. * Keyed on the raw YAML string (file contents), these caches avoid redundant @@ -27,9 +37,9 @@ function asJsonSchema(schema: Record): Parameters(); -const definitionParsedCache = new Map(); -const packageMarkerParsedCache = new Map(); +const rootApiParsedCache = new Map>(); +const definitionParsedCache = new Map>(); +const packageMarkerParsedCache = new Map>(); export declare namespace validateStructureOfYamlFiles { export type Return = SuccessfulResult | FailedResult; @@ -85,18 +95,19 @@ export function validateStructureOfYamlFiles({ }; if (relativeFilepath === ROOT_API_FILENAME) { - // Check cache first + // Check cache: only use a cached entry if it was fully validated, + // or if the caller is skipping validation anyway. const cached = rootApiParsedCache.get(file.rawContents); - if (cached != null) { + if (cached != null && (cached.validated || skipValidation)) { rootApiFile = { - defaultUrl: cached["default-url"], - contents: cached, + defaultUrl: cached.value["default-url"], + contents: cached.value, rawContents: file.rawContents }; } else if (skipValidation) { // Skip JSON schema validation, just run parseOrThrow const contents = RawSchemas.serialization.RootApiFileSchema.parseOrThrow(parsedFileContents); - rootApiParsedCache.set(file.rawContents, contents); + rootApiParsedCache.set(file.rawContents, { value: contents, validated: false }); rootApiFile = { defaultUrl: contents["default-url"], contents, @@ -106,7 +117,7 @@ export function validateStructureOfYamlFiles({ const result = validateAgainstJsonSchema(parsedFileContents, asJsonSchema(RootApiFileJsonSchema)); if (result.success) { const contents = RawSchemas.serialization.RootApiFileSchema.parseOrThrow(parsedFileContents); - rootApiParsedCache.set(file.rawContents, contents); + rootApiParsedCache.set(file.rawContents, { value: contents, validated: true }); rootApiFile = { defaultUrl: contents["default-url"], contents, @@ -117,17 +128,18 @@ export function validateStructureOfYamlFiles({ } } } else if (path.basename(relativeFilepath) === FERN_PACKAGE_MARKER_FILENAME) { - // Check cache first + // Check cache: only use a cached entry if it was fully validated, + // or if the caller is skipping validation anyway. const cached = packageMarkerParsedCache.get(file.rawContents); - if (cached != null) { + if (cached != null && (cached.validated || skipValidation)) { packageMarkers[relativeFilepath] = { - defaultUrl: typeof cached.export === "object" ? cached.export.url : undefined, - contents: cached, + defaultUrl: typeof cached.value.export === "object" ? cached.value.export.url : undefined, + contents: cached.value, rawContents: file.rawContents }; } else if (skipValidation) { const contents = RawSchemas.serialization.PackageMarkerFileSchema.parseOrThrow(parsedFileContents); - packageMarkerParsedCache.set(file.rawContents, contents); + packageMarkerParsedCache.set(file.rawContents, { value: contents, validated: false }); packageMarkers[relativeFilepath] = { defaultUrl: typeof contents.export === "object" ? contents.export.url : undefined, contents, @@ -137,7 +149,7 @@ export function validateStructureOfYamlFiles({ const result = validateAgainstJsonSchema(parsedFileContents, asJsonSchema(PackageMarkerFileJsonSchema)); if (result.success) { const contents = RawSchemas.serialization.PackageMarkerFileSchema.parseOrThrow(parsedFileContents); - packageMarkerParsedCache.set(file.rawContents, contents); + packageMarkerParsedCache.set(file.rawContents, { value: contents, validated: true }); packageMarkers[relativeFilepath] = { defaultUrl: typeof contents.export === "object" ? contents.export.url : undefined, contents, @@ -148,18 +160,19 @@ export function validateStructureOfYamlFiles({ } } } else { - // Check cache first + // Check cache: only use a cached entry if it was fully validated, + // or if the caller is skipping validation anyway. const cached = definitionParsedCache.get(file.rawContents); - if (cached != null) { + if (cached != null && (cached.validated || skipValidation)) { namesDefinitionFiles[relativeFilepath] = { defaultUrl: undefined, - contents: cached, + contents: cached.value, rawContents: file.rawContents, absoluteFilePath: join(absolutePathToDefinition, relativeFilepath) }; } else if (skipValidation) { const contents = RawSchemas.serialization.DefinitionFileSchema.parseOrThrow(parsedFileContents); - definitionParsedCache.set(file.rawContents, contents); + definitionParsedCache.set(file.rawContents, { value: contents, validated: false }); namesDefinitionFiles[relativeFilepath] = { defaultUrl: undefined, contents, @@ -170,7 +183,7 @@ export function validateStructureOfYamlFiles({ const result = validateAgainstJsonSchema(parsedFileContents, asJsonSchema(DefinitionFileJsonSchema)); if (result.success) { const contents = RawSchemas.serialization.DefinitionFileSchema.parseOrThrow(parsedFileContents); - definitionParsedCache.set(file.rawContents, contents); + definitionParsedCache.set(file.rawContents, { value: contents, validated: true }); namesDefinitionFiles[relativeFilepath] = { defaultUrl: undefined, contents,