UNPKG

studiocms

Version:

Astro Native CMS for AstroDB. Built from the ground up by the Astro community.

193 lines (181 loc) 7.87 kB
import { Effect, type ParseResult, pipe, Schema } from '../../../effect.js'; import type { PluginDataEntry, ValidatorOptions } from '../types/index.js'; export type RecursiveSimplifyMutable<A> = { -readonly [K in keyof A]: A[K] extends object ? RecursiveSimplifyMutable<A[K]> : A[K]; } extends infer B ? B : never; /** * Enum representing the possible responses when selecting plugin data, * indicating whether the existence of the data should cause a failure or not. * * @enum {string} * @property {string} ExistsNoFail - The plugin data exists and should not cause a failure. * @property {string} ExistsShouldFail - The plugin data exists and should cause a failure. */ export enum SelectPluginDataRespondOrFail { ExistsNoFail = 'existsNoFail', ExistsShouldFail = 'existsShouldFail', NotExistsShouldFail = 'notExistsShouldFail', } /** * Wraps the provided `id` and `data` into a `PluginDataEntry<T>` object and returns it as an Effect. * * @template T - The type of the data to be wrapped. * @param id - The unique identifier for the plugin data entry. * @param data - The data to be associated with the given id. * @returns An Effect that, when executed, yields a `PluginDataEntry<T>` containing the provided id and data. */ export const parsedDataResponse = <T extends object>( id: string, data: T ): Effect.Effect<PluginDataEntry<T>, never, never> => Effect.succeed({ id, data, }); /** * Filters out `undefined` and `null` values from an array of entries. * * @typeParam T - The type of the array elements. * @param entries - An array containing elements of type `T` or `undefined`. * @returns A new array containing only the defined (non-`undefined`, non-`null`) entries of type `T`. */ export function noUndefinedEntries<T>(entries: (T | undefined)[]) { return entries.filter((entry) => entry !== undefined && entry !== null) as T[]; } /** * Returns a function that validates a boolean condition and either returns the provided value * cast to type `T` if the condition is true, or throws an error if the condition is false. * * @typeParam T - The expected type of the validated object. * @param data - The value to be validated and potentially returned as type `T`. * @returns A function that takes a boolean indicating validation success. * @throws {Error} If the boolean argument is false, throws an error with the serialized value. * * @example * ```typescript * const validateUser = isJsonValid<User>(userData); * const user = validateUser(isUserValid); // Returns userData as User if valid, otherwise throws. * ``` */ export const isJsonValid = <T extends object>(data: unknown) => (isValid: boolean) => { if (isValid) return data as T; throw new Error('Validation failed for plugin data'); }; /** * Returns a validator function based on the provided validator options. * * This function supports three types of validators: * - `jsonFn`: A custom JSON validation function. * - `effectSchema`: An Effect schema for validation. * - `zodSchema`: A Zod schema for validation. * * The returned validator function takes unknown data and attempts to validate it * according to the specified validator. If validation succeeds, the data is returned * as type `T`. If validation fails, an error is thrown or returned as an Effect error. * * @typeParam T - The expected type of the validated data. * @param validator - The validator options, which must include one of: `jsonFn`, `effectSchema`, or `zodSchema`. * @returns A function that takes unknown data and returns an Effect that resolves to type `T` if validation succeeds, or fails with an error if validation fails. * @throws Error if none of the expected validator options are provided. */ export const getValidatorFn = Effect.fn('studiocms/sdk/effect/pluginUtils/getValidatorFn')( function* <T extends Schema.Struct<Schema.Struct.Fields> | object>( validator: ValidatorOptions<T> ) { if ('jsonFn' in validator) { // Return the JSON validator function return (data: unknown) => Effect.try({ try: () => pipe(validator.jsonFn(data), isJsonValid<T>(data)), catch: (error) => new Error(`JSON validation failed: ${(error as Error).message}`), }); } if ('effectSchema' in validator) { // Return the Effect schema validator function return (data: unknown) => Schema.decodeUnknown(validator.effectSchema)(data).pipe( Effect.mapError( (error) => new Error(`Schema validation failed: ${(error as ParseResult.ParseError).message}`) ) ) as Effect.Effect<T, Error, never>; } if ('zodSchema' in validator) { // Return the Zod schema validator function return (data: unknown) => Effect.try({ try: () => { const result = validator.zodSchema.safeParse(data); if (result.success) { return result.data as T; } throw new Error(`Zod validation failed: ${result.error.message}`, { cause: result.error.cause, }); }, catch: (error) => new Error((error as Error).message, { cause: (error as Error).cause }), }); } // If something else is provided, throw an error // This ensures that the validator options are strictly typed and cannot // be accidentally misconfigured or used incorrectly. return yield* Effect.fail( new Error( 'Invalid validator options provided, expected one of: jsonFn, effectSchema, or zodSchema' ) ); } ); /** * Parses and validates plugin data from a raw input, supporting multiple validation strategies. * * This function attempts to parse the provided `rawData`, which can be either a JSON string or an object. * If a validator is provided, it validates the parsed data using one of the supported validation methods: * - JSON function (`jsonFn`) * - Effect schema (`effectSchema`) * - Zod schema (`zodSchema`) * * If no validator is provided, the parsed data is returned as is. * If validation fails or the input format is invalid, an error is yielded. * * @typeParam T - The expected type of the parsed and validated data. * @param rawData - The raw input data, which can be a JSON string or an object. * @param validator - Optional. An object specifying the validation strategy to use. * @returns An `Effect` yielding the parsed and validated data of type `T`, or an error if parsing or validation fails. * * @throws {Error} If the input is neither a string nor an object, or if parsing/validation fails. */ export const parseData = Effect.fn('studiocms/sdk/effect/pluginUtils/parseData')(function* < T extends Schema.Struct<Schema.Struct.Fields> | object, >(rawData: unknown, validator?: ValidatorOptions<T>) { let parsedInput: unknown; // Check if rawData is a string or an object // Data from the db should already be a object, but we handle strings for flexibility if (typeof rawData === 'string') { parsedInput = yield* Effect.try({ try: () => JSON.parse(rawData), catch: (error) => new Error(`JSON parsing failed: ${error}`), }); // Ensure parsedInput is an object // If rawData is not a string, we assume it's already an object } else if (rawData !== null && typeof rawData === 'object') { parsedInput = rawData; } else { // If rawData is neither a string nor a valid object, throw an error return yield* Effect.fail(new Error(`Invalid plugin data format: ${typeof rawData}`)); } if (!validator || validator === undefined) { // If no options are provided, return the parsed input as is return parsedInput as T; } // If a validator is provided, get the validation function const validatorFn = yield* getValidatorFn<T>(validator); // Validate the parsed input using the validator function // If validation fails, it will throw an error which will be caught by the Effect framework // If validation succeeds, it will return the parsed data as type T return yield* validatorFn(parsedInput); });