diff --git a/.changeset/tangy-rice-arrive.md b/.changeset/tangy-rice-arrive.md new file mode 100644 index 00000000..2ac75f32 --- /dev/null +++ b/.changeset/tangy-rice-arrive.md @@ -0,0 +1,15 @@ +--- +"@withstudiocms/component-registry": patch +"@withstudiocms/auth-kit": patch +"@withstudiocms/effect": patch +"@withstudiocms/kysely": patch +"@studiocms/devapps": patch +"@studiocms/discord": patch +"@withstudiocms/sdk": patch +"@studiocms/github": patch +"@studiocms/google": patch +"@studiocms/auth0": patch +"studiocms": patch +--- + +Refactors code to handle Effect LSP diagnostic warnings and errors diff --git a/packages/@studiocms/auth0/src/effect/auth0.ts b/packages/@studiocms/auth0/src/effect/auth0.ts index 776359f5..e85b581a 100644 --- a/packages/@studiocms/auth0/src/effect/auth0.ts +++ b/packages/@studiocms/auth0/src/effect/auth0.ts @@ -120,13 +120,12 @@ export class Auth0OAuthAPI extends Effect.Service()('Auth0OAuthAP }) .pipe( Effect.flatMap(Platform.HttpClientResponse.schemaBodyJson(Auth0User)), - Effect.catchAll((error) => - Effect.fail( + Effect.mapError( + (error) => new ValidateAuthCodeError({ provider: Auth0OAuthAPI.ProviderID, message: `Failed to fetch user info: ${error.message}`, }) - ) ) ); }); diff --git a/packages/@studiocms/devapps/src/effects/WordPressAPI/converters.ts b/packages/@studiocms/devapps/src/effects/WordPressAPI/converters.ts index a2885d7c..846d66c2 100644 --- a/packages/@studiocms/devapps/src/effects/WordPressAPI/converters.ts +++ b/packages/@studiocms/devapps/src/effects/WordPressAPI/converters.ts @@ -15,6 +15,7 @@ import { StringConfig, UseBlogPkgConfig, } from './configs.js'; +import { WPAPIError } from './errors.js'; import type { PageData } from './importers.js'; import { Page, Post } from './schema.js'; import { WordPressAPIUtils } from './utils.js'; @@ -52,6 +53,7 @@ const sharedMeta = (data: Post | Page) => ({ showContributors: false, }); +// @effect-diagnostics-next-line leakingRequirements:off export class WordPressAPIConverters extends Effect.Service()( 'WordPressAPIConverters', { @@ -149,10 +151,10 @@ export class WordPressAPIConverters extends Effect.Service {} diff --git a/packages/@studiocms/devapps/src/effects/wpImporter.ts b/packages/@studiocms/devapps/src/effects/wpImporter.ts index 32627e8c..3f544bff 100644 --- a/packages/@studiocms/devapps/src/effects/wpImporter.ts +++ b/packages/@studiocms/devapps/src/effects/wpImporter.ts @@ -4,6 +4,7 @@ import { ImportEndpointConfig, ImportPostsEndpointConfig, } from './WordPressAPI/configs.js'; +import { WPAPIError } from './WordPressAPI/errors.js'; import { WordPressAPI } from './WordPressAPI/importers.js'; const createResponse = (status: number, statusText: string) => @@ -35,7 +36,7 @@ export class WPImporter extends Effect.Service()('WPImporter', { const data = formData.get(name); if ((!optional && !data) || data === null) { - throw yield* Effect.fail(new Error(`Missing required form field: ${name}`)); + return yield* new WPAPIError({ message: `Missing required form field: ${name}` }); } switch (type) { @@ -46,11 +47,11 @@ export class WPImporter extends Effect.Service()('WPImporter', { return (value === 'true' || value === '1' || value === 'yes') as InferType; } default: - throw yield* Effect.fail( - new Error(`Unsupported type '${type}' for form field: ${name}`) - ); + return yield* new WPAPIError({ + message: `Unsupported type '${type}' for form field: ${name}`, + }); } - }) as Effect.Effect, Error, never>; + }) as Effect.Effect, WPAPIError, never>; /** * Handles the POST request for importing data from a WordPress site. diff --git a/packages/@studiocms/discord/src/effect/discord.ts b/packages/@studiocms/discord/src/effect/discord.ts index 93915945..6af0be1c 100644 --- a/packages/@studiocms/discord/src/effect/discord.ts +++ b/packages/@studiocms/discord/src/effect/discord.ts @@ -88,13 +88,12 @@ export class DiscordOAuthAPI extends Effect.Service()('DiscordO }) .pipe( Effect.flatMap(Platform.HttpClientResponse.schemaBodyJson(DiscordUser)), - Effect.catchAll((error) => - Effect.fail( + Effect.mapError( + (error) => new ValidateAuthCodeError({ provider: DiscordOAuthAPI.ProviderID, message: `Failed to fetch user info: ${error.message}`, }) - ) ) ); }); diff --git a/packages/@studiocms/github/src/effect/github.ts b/packages/@studiocms/github/src/effect/github.ts index e95b48c4..9f930875 100644 --- a/packages/@studiocms/github/src/effect/github.ts +++ b/packages/@studiocms/github/src/effect/github.ts @@ -91,13 +91,12 @@ export class GitHubOAuthAPI extends Effect.Service()('GitHubOAut }) .pipe( Effect.flatMap(Platform.HttpClientResponse.schemaBodyJson(GitHubUser)), - Effect.catchAll((error) => - Effect.fail( + Effect.mapError( + (error) => new ValidateAuthCodeError({ provider: GitHubOAuthAPI.ProviderID, message: `Failed to fetch user info: ${error.message}`, }) - ) ) ); }); diff --git a/packages/@studiocms/google/src/effect/google.ts b/packages/@studiocms/google/src/effect/google.ts index bce77099..c546b3de 100644 --- a/packages/@studiocms/google/src/effect/google.ts +++ b/packages/@studiocms/google/src/effect/google.ts @@ -94,13 +94,12 @@ export class GoogleOAuthAPI extends Effect.Service()('GoogleOAut }) .pipe( Effect.flatMap(Platform.HttpClientResponse.schemaBodyJson(GoogleUser)), - Effect.catchAll((error) => - Effect.fail( + Effect.mapError( + (error) => new ValidateAuthCodeError({ provider: GoogleOAuthAPI.ProviderID, message: `Failed to fetch user info: ${error.message}`, }) - ) ) ); }); diff --git a/packages/@withstudiocms/auth-kit/src/errors.ts b/packages/@withstudiocms/auth-kit/src/errors.ts index fc7486fd..68ea6d1b 100644 --- a/packages/@withstudiocms/auth-kit/src/errors.ts +++ b/packages/@withstudiocms/auth-kit/src/errors.ts @@ -176,3 +176,8 @@ export const useUserErrorPromise = (_try: () => Promise): Effect.Effect new UserError({ cause }), }); + +export class AuthKitError extends Data.TaggedError('AuthKitError')<{ + message: string; + cause?: unknown; +}> {} diff --git a/packages/@withstudiocms/auth-kit/src/index.ts b/packages/@withstudiocms/auth-kit/src/index.ts index 2303c288..91ac5f87 100644 --- a/packages/@withstudiocms/auth-kit/src/index.ts +++ b/packages/@withstudiocms/auth-kit/src/index.ts @@ -1,6 +1,7 @@ import { Effect } from '@withstudiocms/effect'; import { Scrypt as _Scrypt } from '@withstudiocms/effect/scrypt'; import { AuthKitOptions, PasswordModConfigFinal, type RawAuthKitConfig } from './config.js'; +import { AuthKitError } from './errors.js'; import { _Encryption, _Password, _Session, _User } from './modules/index.js'; export { Password } from './modules/password.js'; @@ -18,7 +19,11 @@ export const makeScrypt = Effect.fn((config: PasswordModConfigFinal) => const { run } = yield* _Scrypt; return { run }; }).pipe(Effect.provide(_Scrypt.makeLive(config.scrypt))), - catch: (error) => new Error(`Failed to create Scrypt instance: ${(error as Error).message}`), + catch: (error) => + new AuthKitError({ + message: `Failed to create Scrypt instance: ${(error as Error).message}`, + cause: error, + }), }) ); diff --git a/packages/@withstudiocms/auth-kit/src/modules/session.ts b/packages/@withstudiocms/auth-kit/src/modules/session.ts index 7b62b7e7..f81867d5 100644 --- a/packages/@withstudiocms/auth-kit/src/modules/session.ts +++ b/packages/@withstudiocms/auth-kit/src/modules/session.ts @@ -33,22 +33,20 @@ export const Session = (config: SessionConfig) => } as SessionConfig; if (!sessionTools) { - return yield* Effect.fail( - new SessionError({ cause: 'Session tools must be provided in the configuration' }) - ); + return yield* new SessionError({ + cause: 'Session tools must be provided in the configuration', + }); } if (!Number.isFinite(expTime) || expTime <= 0) { - return yield* Effect.fail( - new SessionError({ - cause: 'Invalid session config: expTime must be a positive number (ms)', - }) - ); + return yield* new SessionError({ + cause: 'Invalid session config: expTime must be a positive number (ms)', + }); } if (typeof cookieName !== 'string' || cookieName.trim().length === 0) { - return yield* Effect.fail( - new SessionError({ cause: 'Invalid session config: cookieName must be a non-empty string' }) - ); + return yield* new SessionError({ + cause: 'Invalid session config: cookieName must be a non-empty string', + }); } const expTimeHalf = expTime / 2; diff --git a/packages/@withstudiocms/auth-kit/src/modules/user.ts b/packages/@withstudiocms/auth-kit/src/modules/user.ts index 48a0c795..1a3e630b 100644 --- a/packages/@withstudiocms/auth-kit/src/modules/user.ts +++ b/packages/@withstudiocms/auth-kit/src/modules/user.ts @@ -56,7 +56,7 @@ export const User = ({ Scrypt, session, userTools }: UserConfig) => /* v8 ignore next 3 */ if (!userTools) { - return yield* Effect.fail(new UserError({ cause: 'User tools are not available' })); + return yield* new UserError({ cause: 'User tools are not available' }); } const notifier = userTools.notifier; @@ -189,7 +189,7 @@ export const User = ({ Scrypt, session, userTools }: UserConfig) => const newHash = yield* Password.hashPassword(password); const data = yield* useUserErrorPromise(() => userTools.getUserById(userId)); if (!data) { - return yield* Effect.fail(new UserError({ cause: 'User not found' })); + return yield* new UserError({ cause: 'User not found' }); } const { avatar, email, emailVerified, name, notifications, id, username, url } = data; return yield* useUserErrorPromise(() => @@ -222,9 +222,8 @@ export const User = ({ Scrypt, session, userTools }: UserConfig) => )(function* (userId: string) { const user = yield* useUserErrorPromise(() => userTools.getUserById(userId)); - if (!user) return yield* Effect.fail(new UserError({ cause: 'User not found' })); - if (!user.password) - return yield* Effect.fail(new UserError({ cause: 'User has no password' })); + if (!user) return yield* new UserError({ cause: 'User not found' }); + if (!user.password) return yield* new UserError({ cause: 'User has no password' }); return user.password; }); diff --git a/packages/@withstudiocms/component-registry/src/registry/Registry.ts b/packages/@withstudiocms/component-registry/src/registry/Registry.ts index 290a1c45..034435f7 100644 --- a/packages/@withstudiocms/component-registry/src/registry/Registry.ts +++ b/packages/@withstudiocms/component-registry/src/registry/Registry.ts @@ -109,7 +109,7 @@ export class ComponentRegistry extends Effect.Service()('Comp const _getComponentProps = Effect.fn(function* (componentName: string) { const component = components.get(componentName); if (!component) { - return yield* Effect.fail(new ComponentNotFoundError({ componentName })); + return yield* new ComponentNotFoundError({ componentName }); } return component; }); diff --git a/packages/@withstudiocms/component-registry/src/utils.ts b/packages/@withstudiocms/component-registry/src/utils.ts index 93d314c7..d503154b 100644 --- a/packages/@withstudiocms/component-registry/src/utils.ts +++ b/packages/@withstudiocms/component-registry/src/utils.ts @@ -1,6 +1,7 @@ import { Effect } from '@withstudiocms/effect'; import type { AstroIntegrationLogger } from 'astro'; import { createResolver } from 'astro-integration-kit'; +import { ComponentRegistryError } from './errors.js'; /** * Options for configuring the logger. @@ -112,7 +113,7 @@ export const resolver = Effect.fn(function* (base: string) { try: () => fn(_resolve), catch: (error) => { console.error('Error occurred while resolving component:', error); - return new Error('Failed to resolve component', { cause: error }); + return new ComponentRegistryError({ message: 'Failed to resolve component', cause: error }); }, }) ); diff --git a/packages/@withstudiocms/component-registry/test/utils.test.ts b/packages/@withstudiocms/component-registry/test/utils.test.ts index 75ef01b0..1422e3ad 100644 --- a/packages/@withstudiocms/component-registry/test/utils.test.ts +++ b/packages/@withstudiocms/component-registry/test/utils.test.ts @@ -2,6 +2,7 @@ import { Effect } from '@withstudiocms/effect'; import * as allure from 'allure-js-commons'; import type { AstroIntegrationLogger } from 'astro'; import { describe, expect, test, vi } from 'vitest'; +import type { ComponentRegistryError } from '../src/errors.js'; import { convertHyphensToUnderscores, convertUnderscoresToHyphens, @@ -355,14 +356,11 @@ describe(parentSuiteName, () => { console.log(resultData); - expect(resultData.cause.failure).toBeInstanceOf(Error); - - expect((resultData.cause.failure as unknown as Error).message).toBe( + expect((resultData.cause.failure as ComponentRegistryError).message).toBe( 'Failed to resolve component' ); - expect((resultData.cause.failure as unknown as Error).cause).toBeInstanceOf(Error); // @ts-expect-error - this is a test - expect((resultData.cause.failure as unknown as Error).cause?.message).toBe(errorMsg); + expect((resultData.cause.failure as ComponentRegistryError).cause?.message).toBe(errorMsg); }); }); diff --git a/packages/@withstudiocms/effect/package.json b/packages/@withstudiocms/effect/package.json index 409e5c7d..719b7f9c 100644 --- a/packages/@withstudiocms/effect/package.json +++ b/packages/@withstudiocms/effect/package.json @@ -45,7 +45,7 @@ "dev": "buildkit dev 'src/**/*.ts'", "test": "vitest", "effect-check": "pnpm effect-language-service diagnostics --project tsconfig.tspc.json", - "ci:effect-check": "pnpm effect-check --format github-actions", + "ci:effect-check": "pnpm effect-check --format github-actions", "typecheck": "tspc -p tsconfig.tspc.json" }, "exports": { diff --git a/packages/@withstudiocms/effect/src/astro/context-utils.ts b/packages/@withstudiocms/effect/src/astro/context-utils.ts index 91796ed4..c64e2c4c 100644 --- a/packages/@withstudiocms/effect/src/astro/context-utils.ts +++ b/packages/@withstudiocms/effect/src/astro/context-utils.ts @@ -1,5 +1,13 @@ import type { APIContext } from 'astro'; -import { Effect, Schema } from '../effect.js'; +import { Data, Effect, Schema } from '../effect.js'; + +/** + * Error class for Astro API context related errors. + */ +export class AstroAPIContextError extends Data.TaggedError('AstroAPIContextError')<{ + message: string; + cause?: unknown; +}> {} /** * Reads and parses the JSON body from an API request within the given context. @@ -11,7 +19,8 @@ import { Effect, Schema } from '../effect.js'; export const readAPIContextJson = ({ request }: APIContext): Effect.Effect => Effect.tryPromise({ try: () => request.json(), - catch: () => new Error('Failed to parse JSON from Request'), + catch: (cause) => + new AstroAPIContextError({ message: 'Failed to parse JSON from Request', cause }), }); /** @@ -28,7 +37,7 @@ export const readAPIContextJson = ({ request }: APIContext): Effect.Effect( context: APIContext, schema?: Schema.Schema -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { const json = yield* readAPIContextJson(context); if (schema) return yield* Schema.decodeUnknown(schema)(json); @@ -36,7 +45,9 @@ export const parseAPIContextJson = ( }).pipe( Effect.catchAll((error) => { console.error('Failed to read JSON:', error); - return Effect.fail(new Error('Failed to read JSON')); + return Effect.fail( + new AstroAPIContextError({ message: 'Failed to read JSON', cause: error }) + ); }) ); @@ -51,10 +62,11 @@ export const parseAPIContextJson = ( */ export const readAPIContextFormData = ({ request, -}: APIContext): Effect.Effect => +}: APIContext): Effect.Effect => Effect.tryPromise({ try: () => request.formData(), - catch: () => new Error('Failed to parse formData from Request'), + catch: (cause) => + new AstroAPIContextError({ message: 'Failed to parse formData from Request', cause }), }); /** @@ -70,7 +82,7 @@ export const readAPIContextFormData = ({ export const parseAPIContextFormDataToObject = ( context: APIContext, schema?: Schema.Schema -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { const formData = yield* readAPIContextFormData(context); const entries = Object.fromEntries(formData.entries()); @@ -81,7 +93,9 @@ export const parseAPIContextFormDataToObject = ( }).pipe( Effect.catchAll((error) => { console.error('Failed to read form data:', error); - return Effect.fail(new Error('Failed to read form data')); + return Effect.fail( + new AstroAPIContextError({ message: 'Failed to read form data', cause: error }) + ); }) ); @@ -97,7 +111,7 @@ export const parseAPIContextFormDataToObject = ( export const parseFormDataEntryToString = ( formData: FormData, key: string -): Effect.Effect => +): Effect.Effect => Effect.try({ try: () => { const value = formData.get(key); @@ -106,5 +120,9 @@ export const parseFormDataEntryToString = ( } return value; }, - catch: () => new Error(`Failed to parse FormData entry for key: ${key}`), + catch: (cause) => + new AstroAPIContextError({ + message: `Failed to parse FormData entry for key: ${key}`, + cause, + }), }); diff --git a/packages/@withstudiocms/effect/src/deepmerge.ts b/packages/@withstudiocms/effect/src/deepmerge.ts index 8367fc57..28db90a3 100644 --- a/packages/@withstudiocms/effect/src/deepmerge.ts +++ b/packages/@withstudiocms/effect/src/deepmerge.ts @@ -4,7 +4,12 @@ import { type DeepMergeBuiltInMetaData, type DeepMergeOptions, } from 'deepmerge-ts'; -import { Effect } from './effect.js'; +import { Data, Effect } from './effect.js'; + +export class DeepmergeError extends Data.TaggedError('DeepmergeError')<{ + message: string; + cause?: unknown; +}> {} /** * Provides a custom deep merge utility within an Effect context. @@ -22,9 +27,10 @@ export const deepmergeCustom = Effect.fn((fn: (deepmerge: typeof _deepmergeCu try: () => fn(_deepmergeCustomSrc), /* v8 ignore next 4 */ catch: (cause) => - new Error( - `Failed to run deepmerge callback: ${cause instanceof Error ? cause.message : String(cause)}` - ), + new DeepmergeError({ + message: `Failed to run deepmerge callback: ${cause instanceof Error ? cause.message : String(cause)}`, + cause, + }), }) ); @@ -50,9 +56,10 @@ export const deepmerge = Effect.fn(function* ( return yield* Effect.try({ try: () => fn(_deepmerge), catch: (cause) => - new Error( - `Failed to run deepmerge: ${cause instanceof Error ? cause.message : String(cause)}` - ), + new DeepmergeError({ + message: `Failed to run deepmerge: ${cause instanceof Error ? cause.message : String(cause)}`, + cause, + }), }); }); diff --git a/packages/@withstudiocms/effect/src/smtp.ts b/packages/@withstudiocms/effect/src/smtp.ts index 6909f32d..6aa73130 100644 --- a/packages/@withstudiocms/effect/src/smtp.ts +++ b/packages/@withstudiocms/effect/src/smtp.ts @@ -2,10 +2,15 @@ import _nodemailer from 'nodemailer'; import type Mail from 'nodemailer/lib/mailer'; import type SMTPTransport from 'nodemailer/lib/smtp-transport'; import socks from 'socks'; -import { Brand, Context, Effect, Layer } from './effect.js'; +import { Brand, Context, Data, Effect, Layer } from './effect.js'; export type { Mail, SMTPTransport }; +export class SMTPError extends Data.TaggedError('SMTPError')<{ + message: string; + cause?: unknown; +}> {} + /** * Custom Transport interface for nodemailer that extends the SMTPTransport.Options to * include an optional proxy configuration. @@ -84,9 +89,10 @@ const convertTransporterConfig = Effect.fn((config: TransportConfig) => }; }, catch: (cause) => - new Error( - `Failed to convert TransportConfig: ${cause instanceof Error ? cause.message : String(cause)}` - ), + new SMTPError({ + message: `Failed to convert TransportConfig: ${cause instanceof Error ? cause.message : String(cause)}`, + cause, + }), }) ); @@ -155,9 +161,10 @@ export class SMTPService extends Effect.Service()('SMTPService', { // The cause is checked to see if it's an instance of Error, and if so, // we use its message; otherwise, we convert it to a string. catch: (cause) => - new Error( - `Failed to run nodemailer function: ${cause instanceof Error ? cause.message : String(cause)}` - ), + new SMTPError({ + message: `Failed to run nodemailer function: ${cause instanceof Error ? cause.message : String(cause)}`, + cause, + }), }) ); @@ -203,11 +210,12 @@ export class SMTPService extends Effect.Service()('SMTPService', { return yield* Effect.tryPromise({ try: () => Promise.resolve().then(() => fn(_mailer)), catch: (cause) => - new Error( - `Failed to run Mailer function: ${ + new SMTPError({ + message: `Failed to run Mailer function: ${ cause instanceof Error ? cause.message : String(cause) - }` - ), + }`, + cause, + }), }); }); @@ -225,9 +233,10 @@ export class SMTPService extends Effect.Service()('SMTPService', { // If an error occurs during verification, we throw a new Error with a custom message. // This helps in debugging SMTP configuration issues. catch: (cause) => - new Error( - `Failed to verify SMTP transport: ${cause instanceof Error ? cause.message : String(cause)}` - ), + new SMTPError({ + message: `Failed to verify SMTP transport: ${cause instanceof Error ? cause.message : String(cause)}`, + cause, + }), }); }); @@ -243,9 +252,10 @@ export class SMTPService extends Effect.Service()('SMTPService', { Effect.tryPromise({ try: () => _mailer.sendMail(mailOptions), catch: (cause) => - new Error( - `Failed to send mail: ${cause instanceof Error ? cause.message : String(cause)}` - ), + new SMTPError({ + message: `Failed to send mail: ${cause instanceof Error ? cause.message : String(cause)}`, + cause, + }), }); /** @@ -259,9 +269,10 @@ export class SMTPService extends Effect.Service()('SMTPService', { return yield* Effect.try({ try: () => _mailer.isIdle(), catch: (cause) => - new Error( - `Failed to check if SMTP transport is idle: ${cause instanceof Error ? cause.message : String(cause)}` - ), + new SMTPError({ + message: `Failed to check if SMTP transport is idle: ${cause instanceof Error ? cause.message : String(cause)}`, + cause, + }), }); }); @@ -276,9 +287,10 @@ export class SMTPService extends Effect.Service()('SMTPService', { return yield* Effect.try({ try: () => _mailer.getVersionString(), catch: (cause) => - new Error( - `Failed to get SMTP transport version string: ${cause instanceof Error ? cause.message : String(cause)}` - ), + new SMTPError({ + message: `Failed to get SMTP transport version string: ${cause instanceof Error ? cause.message : String(cause)}`, + cause, + }), }); }); @@ -293,11 +305,12 @@ export class SMTPService extends Effect.Service()('SMTPService', { Effect.try({ try: () => _mailer.close(), catch: (cause) => - new Error( - `Failed to close SMTP transport: ${ + new SMTPError({ + message: `Failed to close SMTP transport: ${ cause instanceof Error ? cause.message : String(cause) - }` - ), + }`, + cause, + }), }); return { diff --git a/packages/@withstudiocms/kysely/src/utils/introspection.ts b/packages/@withstudiocms/kysely/src/utils/introspection.ts index d5a1405b..180a5640 100644 --- a/packages/@withstudiocms/kysely/src/utils/introspection.ts +++ b/packages/@withstudiocms/kysely/src/utils/introspection.ts @@ -55,9 +55,7 @@ export const getDialect = Effect.fn(function* (db: Kysely) { } /* v8 ignore start */ - return yield* Effect.fail( - new DialectDeterminationError({ cause: 'Unable to determine database dialect.' }) - ); + return yield* new DialectDeterminationError({ cause: 'Unable to determine database dialect.' }); }); /* v8 ignore stop */ diff --git a/packages/@withstudiocms/sdk/src/errors.ts b/packages/@withstudiocms/sdk/src/errors.ts new file mode 100644 index 00000000..1ad61976 --- /dev/null +++ b/packages/@withstudiocms/sdk/src/errors.ts @@ -0,0 +1,6 @@ +import { Data } from '@withstudiocms/effect'; + +export class StudioCMSSDKError extends Data.TaggedError('StudioCMSSDKError')<{ + message: string; + cause?: unknown; +}> {} diff --git a/packages/@withstudiocms/sdk/src/lib/pluginUtils.ts b/packages/@withstudiocms/sdk/src/lib/pluginUtils.ts index e9c38aa1..79c9ac2d 100644 --- a/packages/@withstudiocms/sdk/src/lib/pluginUtils.ts +++ b/packages/@withstudiocms/sdk/src/lib/pluginUtils.ts @@ -1,5 +1,6 @@ import { Effect, type ParseResult, pipe, Schema } from '@withstudiocms/effect'; import type { z } from 'zod'; +import { StudioCMSSDKError } from '../errors.js'; import type { StudioCMSPluginData } from '../tables.js'; /** @@ -209,7 +210,11 @@ export const getValidatorFn = Effect.fn('studiocms/sdk/effect/pluginUtils/getVal return (data: unknown) => Effect.try({ try: () => pipe(validator.jsonFn(data), isJsonValid(data)), - catch: (error) => new Error(`JSON validation failed: ${(error as Error).message}`), + catch: (cause) => + new StudioCMSSDKError({ + message: `JSON validation failed: ${(cause as Error).message}`, + cause, + }), }); } if ('effectSchema' in validator) { @@ -217,10 +222,13 @@ export const getValidatorFn = Effect.fn('studiocms/sdk/effect/pluginUtils/getVal return (data: unknown) => Schema.decodeUnknown(validator.effectSchema)(data).pipe( Effect.mapError( - (error) => - new Error(`Schema validation failed: ${(error as ParseResult.ParseError).message}`) + (cause) => + new StudioCMSSDKError({ + message: `Schema validation failed: ${(cause as ParseResult.ParseError).message}`, + cause, + }) ) - ) as Effect.Effect; + ) as Effect.Effect; } if ('zodSchema' in validator) { // Return the Zod schema validator function @@ -235,17 +243,16 @@ export const getValidatorFn = Effect.fn('studiocms/sdk/effect/pluginUtils/getVal cause: result.error.cause, }); }, - catch: (error) => new Error((error as Error).message, { cause: (error as Error).cause }), + catch: (cause) => new StudioCMSSDKError({ message: (cause as Error).message, 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' - ) - ); + return yield* new StudioCMSSDKError({ + message: + 'Invalid validator options provided, expected one of: jsonFn, effectSchema, or zodSchema', + }); } ); @@ -278,7 +285,7 @@ export const parseData = Effect.fn('studiocms/sdk/effect/pluginUtils/parseData') if (typeof rawData === 'string') { parsedInput = yield* Effect.try({ try: () => JSON.parse(rawData), - catch: (error) => new Error(`JSON parsing failed: ${error}`), + catch: (cause) => new StudioCMSSDKError({ message: `JSON parsing failed: ${cause}`, cause }), }); // Ensure parsedInput is an object // If rawData is not a string, we assume it's already an object @@ -286,7 +293,9 @@ export const parseData = Effect.fn('studiocms/sdk/effect/pluginUtils/parseData') 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}`)); + return yield* new StudioCMSSDKError({ + message: `Invalid plugin data format: ${typeof rawData}`, + }); } if (!validator || validator === undefined) { diff --git a/packages/@withstudiocms/sdk/src/modules/diffTracking/index.ts b/packages/@withstudiocms/sdk/src/modules/diffTracking/index.ts index e6b4ec26..aab8ec90 100644 --- a/packages/@withstudiocms/sdk/src/modules/diffTracking/index.ts +++ b/packages/@withstudiocms/sdk/src/modules/diffTracking/index.ts @@ -375,7 +375,7 @@ export const SDKDiffTrackingModule = Effect.gen(function* () { // If not found, fail with an error if (!diffEntry) { - return yield* Effect.fail(new DiffTrackingError({ message: 'Diff entry not found' })); + return yield* new DiffTrackingError({ message: 'Diff entry not found' }); } // Determine what to revert based on the type @@ -389,9 +389,7 @@ export const SDKDiffTrackingModule = Effect.gen(function* () { // Ensure valid IDs are present if (!pageData.end.id || !pageData.start.id) { - return yield* Effect.fail( - new DiffTrackingError({ message: 'Invalid page metadata for revert' }) - ); + return yield* new DiffTrackingError({ message: 'Invalid page metadata for revert' }); } // Update the page data to the start state diff --git a/packages/@withstudiocms/sdk/src/modules/plugins/index.ts b/packages/@withstudiocms/sdk/src/modules/plugins/index.ts index 1b1de2a5..3b061203 100644 --- a/packages/@withstudiocms/sdk/src/modules/plugins/index.ts +++ b/packages/@withstudiocms/sdk/src/modules/plugins/index.ts @@ -2,6 +2,7 @@ import { Effect, Schema } from '@withstudiocms/effect'; import CacheService from '../../cache.js'; import { cacheKeyGetters, cacheTags } from '../../consts.js'; import { DBClientLive } from '../../context.js'; +import { StudioCMSSDKError } from '../../errors.js'; import { type PluginDataEntry, parseData, @@ -401,19 +402,23 @@ export const SDKPluginsModule = Effect.gen(function* () { case SelectPluginDataRespondOrFail.ExistsShouldFail: { // If it exists, fail with an error if (existing) - return yield* Effect.fail(new Error(`Plugin data with ID ${id} already exists.`)); + return yield* new StudioCMSSDKError({ + message: `Plugin data with ID ${id} already exists.`, + }); // If it does not exist, return undefined return undefined; } case SelectPluginDataRespondOrFail.NotExistsShouldFail: { // If it does not exist, fail with an error if (!existing) - return yield* Effect.fail(new Error(`Plugin data with ID ${id} does not exist.`)); + return yield* new StudioCMSSDKError({ + message: `Plugin data with ID ${id} does not exist.`, + }); // If it exists, return undefined return undefined; } default: - return yield* Effect.fail(new Error(`Invalid mode: ${mode}`)); + return yield* new StudioCMSSDKError({ message: `Invalid mode: ${mode}` }); } }); @@ -582,12 +587,12 @@ export const SDKPluginsModule = Effect.gen(function* () { ): { getEntries: ( filter?: (data: PluginDataEntry[]) => PluginDataEntry[] - ) => Effect.Effect[], Error, never>; + ) => Effect.Effect[], StudioCMSSDKError, never>; getEntry: (id: string) => { generatedId: () => Effect.Effect; - select: () => Effect.Effect | undefined, Error, never>; - insert: (data: R) => Effect.Effect, Error, never>; - update: (data: R) => Effect.Effect, Error, never>; + select: () => Effect.Effect | undefined, StudioCMSSDKError, never>; + insert: (data: R) => Effect.Effect, StudioCMSSDKError, never>; + update: (data: R) => Effect.Effect, StudioCMSSDKError, never>; }; }; @@ -607,9 +612,9 @@ export const SDKPluginsModule = Effect.gen(function* () { opts?: UsePluginDataOpts ): { generatedId: () => Effect.Effect; - select: () => Effect.Effect | undefined, Error, never>; - insert: (data: R) => Effect.Effect, Error, never>; - update: (data: R) => Effect.Effect, Error, never>; + select: () => Effect.Effect | undefined, StudioCMSSDKError, never>; + insert: (data: R) => Effect.Effect, StudioCMSSDKError, never>; + update: (data: R) => Effect.Effect, StudioCMSSDKError, never>; }; /** diff --git a/packages/@withstudiocms/sdk/src/modules/util/collectors.ts b/packages/@withstudiocms/sdk/src/modules/util/collectors.ts index 00dc7b2d..4d90f670 100644 --- a/packages/@withstudiocms/sdk/src/modules/util/collectors.ts +++ b/packages/@withstudiocms/sdk/src/modules/util/collectors.ts @@ -341,11 +341,9 @@ export const SDKCollectors = Effect.gen(function* () { ).pipe(Effect.catchTag('UnknownException', (e) => new CollectorError({ cause: e }))); if (!returnData) { - return yield* Effect.fail( - new CollectorError({ - cause: 'Unknown error occurred while resolving storage manager URL', - }) - ); + return yield* new CollectorError({ + cause: 'Unknown error occurred while resolving storage manager URL', + }); } if (metaOnly) { diff --git a/packages/@withstudiocms/sdk/src/modules/util/getFromNPM.ts b/packages/@withstudiocms/sdk/src/modules/util/getFromNPM.ts index eae97b02..e0e262ba 100644 --- a/packages/@withstudiocms/sdk/src/modules/util/getFromNPM.ts +++ b/packages/@withstudiocms/sdk/src/modules/util/getFromNPM.ts @@ -1,4 +1,4 @@ -import { Effect, HTTPClient, Schema } from '@withstudiocms/effect'; +import { Data, Effect, HTTPClient, Schema } from '@withstudiocms/effect'; import { type CacheEntryStatus, CacheService } from '../../cache.js'; import { cacheKeyGetters, cacheTags } from '../../consts.js'; @@ -81,6 +81,11 @@ export const parseNpmRegistryResponse = Effect.fn(function* (response: Response) return yield* Schema.decodeUnknown(NpmRegistryResponseSchema)(data); }); +export class GetFromNPMError extends Data.TaggedError('GetFromNPMError')<{ + message: string; + cause?: unknown; +}> {} + /** * Cache key for NPM package data. */ @@ -114,7 +119,11 @@ export const GetFromNPM = Effect.gen(function* () { const effectFetch = Effect.fn((url: string) => Effect.tryPromise({ try: () => fetch(url), - catch: (error) => new Error(`Failed to fetch NPM package data: ${String(error)}`), + catch: (error) => + new GetFromNPMError({ + message: `Failed to fetch NPM package data: ${String(error)}`, + cause: error, + }), }) ); diff --git a/packages/studiocms/frontend/middleware/utils.ts b/packages/studiocms/frontend/middleware/utils.ts index 01ca9db8..7c292c0e 100644 --- a/packages/studiocms/frontend/middleware/utils.ts +++ b/packages/studiocms/frontend/middleware/utils.ts @@ -1,6 +1,6 @@ import { UserPermissionLevel, type UserSessionData } from '@withstudiocms/auth-kit/types'; import { getLevel } from '@withstudiocms/auth-kit/utils/user'; -import { deepmerge, Effect, genLogger } from '@withstudiocms/effect'; +import { Data, deepmerge, Effect, genLogger } from '@withstudiocms/effect'; import type { DynamicConfigEntry, StudioCMSSiteConfig } from '@withstudiocms/sdk/types'; import type { APIContext } from 'astro'; @@ -114,6 +114,11 @@ function getGeneralLocals(StudioCMS: APIContext['locals']['StudioCMS']): SetLoca const sharedOpts = { mergeArrays: false } as const; +class MiddlewareLocalsError extends Data.TaggedError('MiddlewareLocalsError')<{ + message: string; + cause?: unknown; +}> {} + /** * Updates the `locals.StudioCMS` property of the given API context with new values for a specified key. * @@ -183,6 +188,6 @@ export const setLocals = Effect.fn(function* < break; } default: - return yield* Effect.fail(new Error(`Unknown key: ${key}`)); + return yield* new MiddlewareLocalsError({ message: `Unknown key: ${key}` }); } }); diff --git a/packages/studiocms/frontend/pages/studiocms_api/auth/[provider]/_effects/index.ts b/packages/studiocms/frontend/pages/studiocms_api/auth/[provider]/_effects/index.ts index bfb8619c..ef3c7cc2 100644 --- a/packages/studiocms/frontend/pages/studiocms_api/auth/[provider]/_effects/index.ts +++ b/packages/studiocms/frontend/pages/studiocms_api/auth/[provider]/_effects/index.ts @@ -1,6 +1,7 @@ import { type OAuthProviders, oAuthProviders } from 'studiocms:plugins/auth/providers'; import { Effect, genLogger } from '@withstudiocms/effect'; import type { APIContext, APIRoute } from 'astro'; +import { StudioCMSAPIError } from '#frontend/utils/errors.js'; /** * Creates a standardized HTTP response for authentication provider errors. @@ -39,7 +40,8 @@ async function promisifyFn(fn: APIRoute, context: APIContext): Promise const promisify = Effect.fn(function* (fn: APIRoute, context: APIContext) { return yield* Effect.tryPromise({ try: () => promisifyFn(fn, context), - catch: (error) => new Error(`Failed to execute API route: ${error}`), + catch: (error) => + new StudioCMSAPIError({ message: `Failed to execute API route: ${error}`, cause: error }), }); }); diff --git a/packages/studiocms/frontend/pages/studiocms_api/dashboard/plugins/[plugin].ts b/packages/studiocms/frontend/pages/studiocms_api/dashboard/plugins/[plugin].ts index e00d2a06..59388dc2 100644 --- a/packages/studiocms/frontend/pages/studiocms_api/dashboard/plugins/[plugin].ts +++ b/packages/studiocms/frontend/pages/studiocms_api/dashboard/plugins/[plugin].ts @@ -10,6 +10,7 @@ import { OptionsResponse, pipe, } from '@withstudiocms/effect'; +import { StudioCMSAPIError } from '#frontend/utils/errors.js'; export const { POST, OPTIONS, ALL } = createEffectAPIRoutes( { @@ -50,7 +51,10 @@ export const { POST, OPTIONS, ALL } = createEffectAPIRoutes( } ), catch: (cause) => - new Error('An error occurred while fetching plugin settings page', { cause }), + new StudioCMSAPIError({ + message: 'An error occurred while fetching plugin settings page', + cause, + }), }); if (settingsPage instanceof Response) { diff --git a/packages/studiocms/frontend/utils/errors.ts b/packages/studiocms/frontend/utils/errors.ts new file mode 100644 index 00000000..a8247c43 --- /dev/null +++ b/packages/studiocms/frontend/utils/errors.ts @@ -0,0 +1,6 @@ +import { Data } from 'effect'; + +export class StudioCMSAPIError extends Data.TaggedError('StudioCMSAPIError')<{ + message: string; + cause?: unknown; +}> {} diff --git a/packages/studiocms/frontend/utils/rest-router.ts b/packages/studiocms/frontend/utils/rest-router.ts index 6c1b7792..9b736995 100644 --- a/packages/studiocms/frontend/utils/rest-router.ts +++ b/packages/studiocms/frontend/utils/rest-router.ts @@ -8,6 +8,7 @@ import { import type { APIContext, APIRoute } from 'astro'; import { Effect, type ParseResult, Schema, type SchemaAST } from 'effect'; import type { NonEmptyReadonlyArray } from 'effect/Array'; +import { StudioCMSAPIError } from './errors.js'; import { extractParams } from './param-extractor.js'; /** @@ -231,7 +232,10 @@ const processHandler = ( const response = yield* Effect.tryPromise({ try: async () => await handler(ctx), catch: (error) => - new Error(`Error in handler for path ${path} [${method}]: ${String(error)}`), + new StudioCMSAPIError({ + message: `Error in handler for path ${path} [${method}]: ${String(error)}`, + cause: error, + }), }).pipe( Effect.catchAll((error) => Effect.logError(`API Route Error: ${String(error)}`).pipe( diff --git a/packages/studiocms/src/cli/add/index.ts b/packages/studiocms/src/cli/add/index.ts index 8b48b63d..030fe1c6 100644 --- a/packages/studiocms/src/cli/add/index.ts +++ b/packages/studiocms/src/cli/add/index.ts @@ -8,6 +8,7 @@ import { getDefaultExportOptions } from 'magicast/helpers'; import { configPaths } from '../../consts.js'; import { Cli, Console, Effect, genLogger, Layer } from '../../effect.js'; import { CliContext, genContext } from '../utils/context.js'; +import { StudioCMSCliError } from '../utils/errors.js'; import { logger } from '../utils/logger.js'; import { TryToInstallPlugins } from './tryToInstallPlugins.js'; import { UpdateStudioCMSConfig } from './updateStudioCMSConfig.js'; @@ -235,7 +236,7 @@ export const addPlugin = Cli.Command.make( CliContext.makeProvide(context), Effect.catchAll((err) => { logger.debug(`Error updating studiocms config ${err}`); - return Effect.fail(createPrettyError(err as Error)); + return new StudioCMSCliError(createPrettyError(err as Error)); }) ); } diff --git a/packages/studiocms/src/cli/add/tryToInstallPlugins.ts b/packages/studiocms/src/cli/add/tryToInstallPlugins.ts index 3a1fe652..a2500a87 100644 --- a/packages/studiocms/src/cli/add/tryToInstallPlugins.ts +++ b/packages/studiocms/src/cli/add/tryToInstallPlugins.ts @@ -4,6 +4,7 @@ import { detect, resolveCommand } from 'package-manager-detector'; import { Console, Effect, genLogger } from '../../effect.js'; import { CliContext } from '../utils/context.js'; import { effectBoxen } from '../utils/effectBoxen.js'; +import { StudioCMSCliError } from '../utils/errors.js'; import { type PluginInfo, UpdateResult } from './index.js'; import { convertPluginsToInstallSpecifiers } from './npm-utils.js'; @@ -78,10 +79,10 @@ export class TryToInstallPlugins extends Effect.Service()( return UpdateResult.updated; }, catch: (err) => - new Error( - `Failed to install dependencies: ${err instanceof Error ? err.message : String(err)}`, - { cause: err } - ), + new StudioCMSCliError({ + message: `Failed to install dependencies: ${err instanceof Error ? err.message : String(err)}`, + cause: err, + }), }).pipe(Effect.catchAll((err) => Effect.succeed(err))); if (response === UpdateResult.updated) { diff --git a/packages/studiocms/src/cli/crypto/genJWT/index.ts b/packages/studiocms/src/cli/crypto/genJWT/index.ts index 52b281ba..bc849f0c 100644 --- a/packages/studiocms/src/cli/crypto/genJWT/index.ts +++ b/packages/studiocms/src/cli/crypto/genJWT/index.ts @@ -12,6 +12,7 @@ import { Cli, Effect, genLogger } from '../../../effect.js'; import { genContext } from '../../utils/context.js'; import { dateAdd } from '../../utils/dateAdd.js'; import { debug } from '../../utils/debugOpt.js'; +import { StudioCMSCliError } from '../../utils/errors.js'; import { logger } from '../../utils/logger.js'; /** @@ -62,11 +63,13 @@ export const genJWT = Cli.Command.make( } if (Number.isNaN(exp)) { - return yield* Effect.fail(new Error(`Expiration must be a valid number, received: ${exp}`)); + return yield* new StudioCMSCliError({ + message: `Expiration must be a valid number, received: ${exp}`, + }); } if (exp < 0) { - return yield* Effect.fail(new Error('Expiration must be greater than 0')); + return yield* new StudioCMSCliError({ message: 'Expiration must be greater than 0' }); } if (debug) { @@ -101,7 +104,7 @@ export const genJWT = Cli.Command.make( if (!keyString) { yield* spin.stop('Key not found, or invalid'); - return yield* Effect.fail(new Error('Key not found, or invalid')); + return yield* new StudioCMSCliError({ message: 'Key not found, or invalid' }); } const alg = 'EdDSA'; @@ -111,7 +114,7 @@ export const genJWT = Cli.Command.make( privateKey = yield* Effect.tryPromise(() => importPKCS8(keyString, alg)); } catch (_e) { yield* spin.stop('Invalid or unsupported private key'); - return yield* Effect.fail(new Error('Invalid or unsupported private key')); + return yield* new StudioCMSCliError({ message: 'Invalid or unsupported private key' }); } const NOW = new Date(); @@ -153,7 +156,7 @@ export const genJWT = Cli.Command.make( } else { yield* log.error(`Unexpected error generating JWT: ${err}`); } - return yield* Effect.fail(new Error('JWT ERROR: Unknown')); + return yield* new StudioCMSCliError({ message: 'JWT ERROR: Unknown' }); } }) ).pipe(Cli.Command.withDescription('Generate a JWT token from a keyfile')); diff --git a/packages/studiocms/src/cli/debug/index.ts b/packages/studiocms/src/cli/debug/index.ts index c2873b18..bddc5cb1 100644 --- a/packages/studiocms/src/cli/debug/index.ts +++ b/packages/studiocms/src/cli/debug/index.ts @@ -9,6 +9,7 @@ import type { AstroUserConfig } from 'astro'; import chalk from 'chalk'; import type { StudioCMSOptions } from '#schemas'; import { debug } from '../utils/debugOpt.js'; +import { StudioCMSCliError } from '../utils/errors.js'; const astroConfigPaths = [ 'astro.config.mjs', @@ -93,12 +94,19 @@ export const debugCMD = Cli.Command.make('debug', { debug }, ({ debug: _debug }) installedPlugins, }), catch: (error) => - new Error(`Failed to create DebugInfoProvider: ${(error as Error).message}`), + new StudioCMSCliError({ + message: `Failed to create DebugInfoProvider: ${(error as Error).message}`, + cause: error, + }), }); const debugInfo = yield* Effect.tryPromise({ try: () => infoProvider.getDebugInfoString(4, { styled: true }), - catch: (error) => new Error(`Failed to gather debug info: ${(error as Error).message}`), + catch: (error) => + new StudioCMSCliError({ + message: `Failed to gather debug info: ${(error as Error).message}`, + cause: error, + }), }); if (debug) { diff --git a/packages/studiocms/src/cli/getTurso/index.ts b/packages/studiocms/src/cli/getTurso/index.ts index e24b2e50..e29ca0bf 100644 --- a/packages/studiocms/src/cli/getTurso/index.ts +++ b/packages/studiocms/src/cli/getTurso/index.ts @@ -1,5 +1,6 @@ import { runInteractiveCommand } from '@withstudiocms/cli-kit/utils'; import { Cli, Effect } from '../../effect.js'; +import { StudioCMSCliError } from '../utils/errors.js'; const shell = Cli.Args.text({ name: 'shell' }).pipe( Cli.Args.optional, @@ -23,7 +24,11 @@ export const getTurso = Cli.Command.make('get-turso', { shell }, ({ shell: rawSh runInteractiveCommand(command).then(() => console.log('Turso CLI install command completed.') ), - catch: (error) => new Error(`Failed to install Turso CLI: ${String(error)}`), + catch: (error) => + new StudioCMSCliError({ + message: `Failed to install Turso CLI: ${String(error)}`, + cause: error, + }), }); }) ).pipe(Cli.Command.withDescription('Install the Turso CLI')); diff --git a/packages/studiocms/src/cli/users/steps/createUsers.ts b/packages/studiocms/src/cli/users/steps/createUsers.ts index 494532a3..99b3183c 100644 --- a/packages/studiocms/src/cli/users/steps/createUsers.ts +++ b/packages/studiocms/src/cli/users/steps/createUsers.ts @@ -4,6 +4,7 @@ import { group, log, password, select, text } from '@withstudiocms/effect/clack' import { StudioCMSPermissions, StudioCMSUsersTable } from '@withstudiocms/sdk/tables'; import { z } from 'astro/zod'; import { Effect, runEffect, Schema } from '../../../effect.js'; +import { StudioCMSCliError } from '../../utils/errors.js'; import { getCliDbClient } from '../../utils/getCliDbClient.js'; import type { EffectStepFn } from '../../utils/types.js'; import { getCheckers, hashPassword, verifyPasswordStrength } from '../../utils/user-utils.js'; @@ -19,12 +20,12 @@ import { validateInputOrRePrompt } from '../shared.js'; * @return An effect representing the user creation step. */ export const createUsers: EffectStepFn = Effect.fn(function* (context, _debug, dryRun) { - // @effect-diagnostics-next-line globalErrorInEffectFailure:off const [checker, dbClient] = yield* Effect.all([ getCheckers, getCliDbClient(context).pipe( - Effect.catchAll((error) => - Effect.fail(new Error(`Failed to get database client: ${error.message}`)) + Effect.mapError( + (error) => + new StudioCMSCliError({ message: `Failed to get database client: ${error.message}` }) ) ), ]); @@ -190,7 +191,7 @@ export const createUsers: EffectStepFn = Effect.fn(function* (context, _debug, d hashPassword(newPassword), Effect.tryPromise({ try: () => getAvatarUrl({ email, https: true, size: 400, default: 'retro' }), - catch: (cause) => new Error('Failed to fetch avatar URL', { cause }), + catch: (cause) => new StudioCMSCliError({ message: 'Failed to fetch avatar URL', cause }), }), ]); diff --git a/packages/studiocms/src/cli/users/steps/modifyUsers.ts b/packages/studiocms/src/cli/users/steps/modifyUsers.ts index 59bcb0f9..4c642901 100644 --- a/packages/studiocms/src/cli/users/steps/modifyUsers.ts +++ b/packages/studiocms/src/cli/users/steps/modifyUsers.ts @@ -3,6 +3,7 @@ import { runEffect } from '@withstudiocms/effect'; import { log, note, password, select, text } from '@withstudiocms/effect/clack'; import { StudioCMSPermissions, StudioCMSUsersTable } from '@withstudiocms/sdk/tables'; import { Effect, Schema } from 'effect'; +import { StudioCMSCliError } from '../../utils/errors.js'; import { getCliDbClient } from '../../utils/getCliDbClient.js'; import type { EffectStepFn } from '../../utils/types.js'; import { getCheckers, hashPassword, verifyPasswordStrength } from '../../utils/user-utils.js'; @@ -18,8 +19,9 @@ export const modifyUsers: EffectStepFn = Effect.fn(function* (context, _debug, d const [checker, dbClient] = yield* Effect.all([ getCheckers, getCliDbClient(context).pipe( - Effect.catchAll((error) => - Effect.fail(new Error(`Failed to get database client: ${error.message}`)) + Effect.mapError( + (error) => + new StudioCMSCliError({ message: `Failed to get database client: ${error.message}` }) ) ), ]); diff --git a/packages/studiocms/src/cli/utils/dateAdd.ts b/packages/studiocms/src/cli/utils/dateAdd.ts index 665b9452..fa85f5b6 100644 --- a/packages/studiocms/src/cli/utils/dateAdd.ts +++ b/packages/studiocms/src/cli/utils/dateAdd.ts @@ -1,4 +1,5 @@ import { Effect } from '../../effect.js'; +import { StudioCMSCliError } from './errors.js'; export type Interval = 'year' | 'quarter' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second'; @@ -56,8 +57,9 @@ export const dateAdd = (date: Date, interval: Interval, units: number) => return ret; }, catch: () => - new RangeError( - `dateAdd: unsupported interval "${interval}". ` + - `Expected one of ${['year', 'quarter', 'month', 'week', 'day', 'hour', 'minute', 'second'].join(', ')}.` - ), + new StudioCMSCliError({ + message: + `dateAdd: unsupported interval "${interval}". ` + + `Expected one of ${['year', 'quarter', 'month', 'week', 'day', 'hour', 'minute', 'second'].join(', ')}.`, + }), }); diff --git a/packages/studiocms/src/cli/utils/effectBoxen.ts b/packages/studiocms/src/cli/utils/effectBoxen.ts index 6665ed63..82fc8178 100644 --- a/packages/studiocms/src/cli/utils/effectBoxen.ts +++ b/packages/studiocms/src/cli/utils/effectBoxen.ts @@ -1,5 +1,6 @@ import _boxen from 'boxen'; import { Effect } from '../../effect.js'; +import { StudioCMSCliError } from './errors.js'; /** Creates a box in the terminal. @@ -42,8 +43,9 @@ export const effectBoxen = Effect.fn((fn: (boxen: typeof _boxen) => T) => Effect.try({ try: () => fn(_boxen), catch: (cause) => - new Error( - `Boxen Error: Failed to run boxen: ${cause instanceof Error ? cause.message : String(cause)}` - ), + new StudioCMSCliError({ + message: `Boxen Error: Failed to run boxen: ${cause instanceof Error ? cause.message : String(cause)}`, + cause, + }), }) ); diff --git a/packages/studiocms/src/cli/utils/errors.ts b/packages/studiocms/src/cli/utils/errors.ts new file mode 100644 index 00000000..07ef816b --- /dev/null +++ b/packages/studiocms/src/cli/utils/errors.ts @@ -0,0 +1,6 @@ +import { Data } from 'effect'; + +export class StudioCMSCliError extends Data.TaggedError('StudioCMSCliError')<{ + message: string; + cause?: unknown; +}> {} diff --git a/packages/studiocms/src/db/index.ts b/packages/studiocms/src/db/index.ts index feef42ff..8cade141 100644 --- a/packages/studiocms/src/db/index.ts +++ b/packages/studiocms/src/db/index.ts @@ -85,7 +85,7 @@ export const getDbDriver = Effect.fn(function* (dialect: DbDialect) { return yield* driverModule.mysqlDriver; } default: - return yield* Effect.fail(new UnsupportedDialectError({ dialect })); + return yield* new UnsupportedDialectError({ dialect }); } }); diff --git a/packages/studiocms/src/virtuals/auth/verify-email.ts b/packages/studiocms/src/virtuals/auth/verify-email.ts index 9f53e6da..a0ffcca6 100644 --- a/packages/studiocms/src/virtuals/auth/verify-email.ts +++ b/packages/studiocms/src/virtuals/auth/verify-email.ts @@ -8,7 +8,10 @@ import { CMSNotificationSettingsId } from '../../consts.js'; import { Data, Effect, genLogger, pipeLogger } from '../../effect.js'; import type { UserSessionData } from './types.js'; -export class VerifyEmailError extends Data.TaggedError('VerifyEmailError')<{ message: string }> {} +export class VerifyEmailError extends Data.TaggedError('VerifyEmailError')<{ + message: string; + cause?: unknown; +}> {} /** * The `VerifyEmail` service provides functionality for managing email verification @@ -164,7 +167,7 @@ export class VerifyEmail extends Effect.Service()( const siteConfig = yield* sdk.GET.siteConfig(); if (!siteConfig) { - return yield* Effect.fail(new Error('Site configuration not found')); + return yield* new VerifyEmailError({ message: 'Site configuration not found' }); } const { data: config } = siteConfig; @@ -174,17 +177,19 @@ export class VerifyEmail extends Effect.Service()( } if (!user) { - return yield* Effect.fail(new Error('User not found')); + return yield* new VerifyEmailError({ message: 'User not found' }); } const verificationToken = yield* createEmailVerificationRequest(userId); if (!verificationToken) { - return yield* Effect.fail(new Error('Failed to create verification token')); + return yield* new VerifyEmailError({ + message: 'Failed to create verification token', + }); } const email = user.email; if (!email) { - return yield* Effect.fail(new Error('User does not have an email')); + return yield* new VerifyEmailError({ message: 'User does not have an email' }); } const verifyLink = yield* generateUrl( diff --git a/packages/studiocms/src/virtuals/template-engine/index.ts b/packages/studiocms/src/virtuals/template-engine/index.ts index 0f681d5b..f50a817c 100644 --- a/packages/studiocms/src/virtuals/template-engine/index.ts +++ b/packages/studiocms/src/virtuals/template-engine/index.ts @@ -5,7 +5,7 @@ */ import { SDKCore } from 'studiocms:sdk'; import TemplateEngine from '@withstudiocms/template-lang'; -import { Effect } from '../../effect.js'; +import { Data, Effect } from '../../effect.js'; import defaultTemplates from './default-templates.js'; export type EngineContext = { @@ -19,6 +19,11 @@ export type EngineContext = { const engine = new TemplateEngine({ strict: true }); +export class TemplateEngineError extends Data.TaggedError('TemplateEngineError')<{ + message: string; + cause?: unknown; +}> {} + /** * The template engine provides functionality to manage and render email templates. * @@ -33,7 +38,9 @@ export const templateEngine = Effect.gen(function* () { config = yield* sdk.CONFIG.templateConfig.init(defaultTemplates); } if (!config) { - return yield* Effect.fail(new Error('Failed to initialize template configuration.')); + return yield* new TemplateEngineError({ + message: 'Failed to initialize template configuration.', + }); } // Extract the templates from the configuration data @@ -84,8 +91,11 @@ export const templateEngine = Effect.gen(function* () { const template = templates[key] || defaultTemplates[key]; return Effect.try({ try: () => engine.render(template, context), - catch: (error) => - new Error(`Failed to render template "${key}": ${(error as Error).message}`), + catch: (cause) => + new TemplateEngineError({ + message: `Failed to render template "${key}": ${(cause as Error).message}`, + cause, + }), }); }, } as const;