import type { INodeProperties } from 'n8n-workflow'; import z, { type ZodOptional, type ZodType } from 'zod'; function isObject(value: unknown): value is object { return typeof value === 'object' && value !== null && !Array.isArray(value); } function removeUndefinedProperties(obj: T): T { for (const [key, value] of Object.entries(obj)) { if (value === undefined) { delete (obj as Record)[key]; } else if (isObject(value)) { removeUndefinedProperties(value); if (Object.keys(value).length === 0) { delete (obj as Record)[key]; } } } return obj; } class CredentialSchemaRootObject< M extends BaseMetadata, S extends ZodType | null, T extends { [k: string]: CredentialSchemaProperty } = {}, > { constructor(private shape: T) {} validate(data: Data) { return this.toZodSchema().safeParse(data); } toZodSchema() { return z.object( Object.fromEntries( Object.entries(this.shape) .filter(([_, property]) => property.toZodSchema()) .map(([key, property]) => [key, property.toZodSchema()]), ) as ZodifyObject, ); } getProperty(key: K): T[K]['metadata'] { return this.shape[key].metadata; } toNodeProperties() { return Object.entries(this.shape).map(([key, schema]) => schema.toNodeProperties(key)); } } type ToZodSchemaReturnType< M extends BaseMetadata = BaseMetadata, S extends ZodType | null = ZodType, > = M['optional'] extends true ? (S extends null ? null : ZodOptional>) : S; abstract class CredentialSchemaProperty< M extends BaseMetadata = BaseMetadata, S extends ZodType | null = null, > { constructor( public metadata: M, public schema: S, ) {} toZodSchema(): ToZodSchemaReturnType { if (this.schema && this.metadata.optional) { return this.schema.optional() as ToZodSchemaReturnType; } return this.schema as ToZodSchemaReturnType; } toNodeProperties(name: string): INodeProperties { return removeUndefinedProperties({ name, displayName: this.metadata.label, description: this.metadata.description, default: '', type: 'string', }); } } class CredentialSchemaString< S extends ZodType, M extends StringMetadata, > extends CredentialSchemaProperty { constructor( public metadata: M, schema: S, ) { super(metadata, schema); } toNodeProperties(name: string): INodeProperties { return removeUndefinedProperties({ ...super.toNodeProperties(name), type: 'string', placeholder: this.metadata.placeholder, typeOptions: { password: this.metadata.password }, }); } } class CredentialSchemaOptions< V extends string, S extends ZodType, M extends OptionsMetadata, > extends CredentialSchemaProperty { constructor( public metadata: M, schema: S, ) { super(metadata, schema); } toNodeProperties(name: string): INodeProperties { const { options } = this.metadata; return removeUndefinedProperties({ ...super.toNodeProperties(name), type: 'options', options: options.map((option) => ({ name: option.label, value: option.value, description: option.description, })), default: options.find((option) => option.default)?.value ?? options[0].value, }); } } class CredentialSchemaNotice extends CredentialSchemaProperty { constructor(public notice: string) { super({ label: notice }, null); } toNodeProperties(name: string): INodeProperties { return { ...super.toNodeProperties(name), type: 'notice', }; } } type BaseMetadata = { label: string; description?: string; default?: unknown; optional?: boolean; }; type StringMetadata = BaseMetadata & Partial<{ password: boolean; placeholder: string; default: string; }>; type Option = { label: string; value: V; default?: boolean; description?: string; }; type NonEmptyArray = [T, ...T[]]; type OptionsMetadata = BaseMetadata & { options: NonEmptyArray> }; type Zodify< M extends BaseMetadata, S extends ZodType | null, T extends CredentialSchemaProperty, > = ReturnType extends z.ZodType ? ReturnType : never; type ZodifyObject< M extends BaseMetadata, S extends ZodType | null, T extends { [k: string]: CredentialSchemaProperty }, > = { [K in keyof T as ReturnType extends z.ZodType ? K : never]: Zodify< M, S, T[K] >; }; type Optional = Omit & Partial>; export const CredentialSchema = { create< M extends BaseMetadata, S extends ZodType | null, T extends { [k: string]: CredentialSchemaProperty }, >(shape: T) { return new CredentialSchemaRootObject(shape); }, password(options: Omit, 'password'> = {}) { return new CredentialSchemaString( { password: true, label: 'Password', ...options, }, z.string(), ); }, // eslint-disable-next-line id-denylist string(options: M) { return new CredentialSchemaString(options, z.string()); }, url(options: Optional = {}) { return new CredentialSchemaString({ label: 'URL', ...options }, z.string().url()); }, email(options: Optional = {}) { return new CredentialSchemaString({ label: 'Email', ...options }, z.string().email()); }, options>(options: M) { return new CredentialSchemaOptions( options, z.enum( options.options.map((option) => option.value) as NonEmptyArray< M['options'][number]['value'] >, ), ); }, notice(notice: string) { return new CredentialSchemaNotice(notice); }, }; export type InferCredentialSchema< T extends CredentialSchemaRootObject, > = z.infer>;