import { Schema } from "./schema.js"; import { SObjectProperties, InferSObjectType, ValidatorConfig, ValidationContext, ValidationIssue, ValidationError, MessageProducer, } from "../types.js"; export class ObjectSchema< P extends SObjectProperties, T = InferSObjectType

> extends Schema { constructor( config: ValidatorConfig & { validate?: { properties?: P } } ) { super("object", config); } public async _prepare(context: ValidationContext): Promise { const preparedValue = await super._prepare(context); if ( preparedValue === null || preparedValue === undefined || typeof preparedValue !== "object" ) { return preparedValue; } const shape = this.getProperties(); const newValue: Record = { ...preparedValue }; for (const key in shape) { if (Object.prototype.hasOwnProperty.call(newValue, key)) { const propertySchema = shape[key]; newValue[key] = await propertySchema._prepare({ rootData: context.rootData, path: [...context.path, key], value: newValue[key], ctx: context.ctx, }); } } return newValue; } public async _validate( value: Record, context: ValidationContext ): Promise { if (this.config.optional && value === undefined) { return undefined; } if (this.config.nullable && value === null) { return null; } // First, run the basic identity check from the base Schema class. // This checks if the value is a non-null object. await super._validate(value, context); const shape = this.getProperties(); const strict = this.config.strict as boolean; const issues: ValidationIssue[] = []; const newValue: Record = {}; const propertyPromises = Object.keys(shape).map(async (key) => { const propertySchema = shape[key]; const propertyValue = value[key]; const newContext = { rootData: context.rootData, path: [...context.path, key], value: propertyValue, ctx: context.ctx, }; try { if (Object.prototype.hasOwnProperty.call(value, key)) { const validatedValue = await propertySchema._validate( propertyValue, newContext ); newValue[key] = validatedValue; } else if (!propertySchema.config.optional) { issues.push({ path: newContext.path, message: `Required property '${key}' is missing`, }); } } catch (error) { if (error instanceof ValidationError) { issues.push(...error.issues); } else { throw error; } } }); await Promise.all(propertyPromises); // Handle unrecognized keys in strict mode. if (strict) { for (const key in value) { if (!shape[key]) { issues.push({ path: [...context.path, key], message: `Unrecognized key: '${key}'`, }); } } } else { // Copy over properties that are not in the schema if not in strict mode. for (const key in value) { if (!shape[key]) { newValue[key] = value[key]; } } } if (issues.length > 0) { throw new ValidationError(issues); } // Now, with a fully parsed and transformed object, run the custom validators. for (const customValidator of this.customValidators) { const customValidatorFn = typeof customValidator === "object" ? customValidator.validator : customValidator; const customMessage = typeof customValidator === "object" ? customValidator.message : undefined; const customValidatorName = typeof customValidator === "object" ? customValidator.name : undefined; if ( !(await customValidatorFn( newValue as T, [], { ...context, value: newValue, }, this )) ) { const messages = (this.config as ValidatorConfig).messages ?? {}; const messageProducerContext = { label: this.label, value: newValue, path: context.path, dataType: this.dataType, ctx: context.ctx, args: [], schema: this, }; let message: string | undefined = typeof customMessage === "function" ? customMessage(messageProducerContext) : customMessage; if (!message) { const userMessage = messages[customValidatorName as keyof typeof messages] ?? messages["custom"]; if (typeof userMessage === "string") { message = userMessage; } else if (typeof userMessage === "function") { message = (userMessage as MessageProducer)(messageProducerContext); } } issues.push({ path: context.path, message: message ?? `Custom validation failed for ${ customValidatorName ?? this.dataType }`, }); } } if (issues.length > 0) { throw new ValidationError(issues); } return newValue; } public async _transform( value: Record, context: ValidationContext ): Promise { const transformedValue = await super._transform(value, context); const shape = this.getProperties(); const newValue: Record = { ...transformedValue }; const transformPromises = Object.keys(shape).map(async (key) => { if (Object.prototype.hasOwnProperty.call(newValue, key)) { const propertySchema = shape[key]; newValue[key] = await propertySchema._transform(newValue[key], { rootData: context.rootData, path: [...context.path, key], value: newValue[key], ctx: context.ctx, }); } }); await Promise.all(transformPromises); return newValue; } private getProperties(): P { const config = this.config as { validate?: { properties?: P } }; return config.validate?.properties ?? ({} as P); } public partial(): ObjectSchema> { const originalProperties = this.getProperties(); const newProperties: { [K in keyof P]?: Schema } = {}; for (const key in originalProperties) { newProperties[key] = originalProperties[key].optional(); } const newConfig = { ...this.config, validate: { ...(this.config.validate as Record), properties: newProperties as P, }, }; return new ObjectSchema(newConfig as any); } public pick( keys: K[] ): ObjectSchema, Pick> { const originalProperties = this.getProperties(); const newProperties: Partial> = {}; for (const key of keys) { if (originalProperties[key]) { newProperties[key] = originalProperties[key]; } } const newConfig = { ...this.config, validate: { ...(this.config.validate as Record), properties: newProperties as Pick, }, strict: true, }; return new ObjectSchema(newConfig as any); } public omit( keys: K[] ): ObjectSchema, Omit> { const originalProperties = this.getProperties(); const newProperties: Partial> = { ...originalProperties }; for (const key of keys) { delete (newProperties as any)[key]; } const newConfig = { ...this.config, validate: { ...(this.config.validate as Record), properties: newProperties as Omit, }, strict: true, }; return new ObjectSchema(newConfig as any); } public extend( extension: E ): ObjectSchema

> { const originalProperties = this.getProperties(); const newProperties = { ...originalProperties, ...extension }; const newConfig = { ...this.config, validate: { ...(this.config.validate as Record), properties: newProperties, }, }; return new ObjectSchema(newConfig as any); } }