Skip to content

Commit

Permalink
refactor: remove types.ts and server.ts files, export types from / an…
Browse files Browse the repository at this point in the history
…d /hooks
  • Loading branch information
TheEdoRan committed Jan 2, 2024
1 parent 6f64ae1 commit 49d229f
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 151 deletions.
6 changes: 1 addition & 5 deletions packages/next-safe-action/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
],
"exports": {
".": "./dist/index.mjs",
"./hooks": "./dist/hooks.mjs",
"./types": "./dist/types.mjs"
"./hooks": "./dist/hooks.mjs"
},
"typesVersions": {
"*": {
Expand All @@ -21,9 +20,6 @@
],
"hooks": [
"./dist/hooks.d.mts"
],
"types": [
"./dist/types.d.mts"
]
}
},
Expand Down
37 changes: 36 additions & 1 deletion packages/next-safe-action/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,42 @@ import { isNotFoundError } from "next/dist/client/components/not-found.js";
import { isRedirectError } from "next/dist/client/components/redirect.js";
import { useCallback, useEffect, useOptimistic, useRef, useState, useTransition } from "react";
import {} from "react/experimental";
import type { HookActionStatus, HookCallbacks, HookResult, SafeAction } from "./types";
import type { SafeAction } from ".";
import type { MaybePromise } from "./utils";
import { isError } from "./utils";

// TYPES

/**
* Type of `result` object returned by `useAction` and `useOptimisticAction` hooks.
*/
export type HookResult<S extends Schema, Data> = Awaited<ReturnType<SafeAction<S, Data>>> & {
fetchError?: string;
};

/**
* Type of hooks callbacks. These are executed when action is in a specific state.
*/
export type HookCallbacks<S extends Schema, Data> = {
onExecute?: (input: InferIn<S>) => MaybePromise<void>;
onSuccess?: (data: Data, input: InferIn<S>, reset: () => void) => MaybePromise<void>;
onError?: (
error: Omit<HookResult<S, Data>, "data">,
input: InferIn<S>,
reset: () => void
) => MaybePromise<void>;
onSettled?: (
result: HookResult<S, Data>,
input: InferIn<S>,
reset: () => void
) => MaybePromise<void>;
};

/**
* Type of the action status returned by `useAction` and `useOptimisticAction` hooks.
*/
export type HookActionStatus = "idle" | "executing" | "hasSucceeded" | "hasErrored";

// UTILS

const DEFAULT_RESULT = {
Expand Down Expand Up @@ -75,6 +108,8 @@ const useActionCallbacks = <const S extends Schema, const Data>(
}, [status, result, reset, input]);
};

// HOOKS

/**
* Use the action from a Client Component via hook.
* @param safeAction The typesafe action.
Expand Down
114 changes: 111 additions & 3 deletions packages/next-safe-action/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,112 @@
import { createSafeActionClient } from "./server";
import { DEFAULT_SERVER_ERROR } from "./utils";
import type { Infer, InferIn, Schema } from "@decs/typeschema";
import { wrap } from "@decs/typeschema";
import { isNotFoundError } from "next/dist/client/components/not-found.js";
import { isRedirectError } from "next/dist/client/components/redirect.js";
import type { MaybePromise } from "./utils";
import { buildValidationErrors, isError } from "./utils";

export { DEFAULT_SERVER_ERROR, createSafeActionClient };
// TYPES

/**
* Type of the function called from Client Components with typesafe input data.
*/
export type SafeAction<S extends Schema, Data> = (input: InferIn<S>) => Promise<{
data?: Data;
serverError?: string;
validationErrors?: Partial<Record<keyof Infer<S> | "_root", string[]>>;
}>;

/**
* Type of the function that executes server code when defining a new safe action.
*/
export type ServerCodeFn<S extends Schema, Data, Context> = (
parsedInput: Infer<S>,
ctx: Context
) => Promise<Data>;

// UTILS

export const DEFAULT_SERVER_ERROR = "Something went wrong while executing the operation";

// SAFE ACTION CLIENT

/**
* Initialize a new action client.
* @param createOpts Options for creating a new action client.
* @returns {Function} A function that creates a new action, to be used in server files.
*
* {@link https://next-safe-action.dev/docs/getting-started See an example}
*/
export const createSafeActionClient = <Context>(createOpts?: {
handleServerErrorLog?: (e: Error) => MaybePromise<void>;
handleReturnedServerError?: (e: Error) => MaybePromise<string>;
middleware?: (parsedInput: unknown) => MaybePromise<Context>;
}) => {
// If server log function is not provided, default to `console.error` for logging
// server error messages.
const handleServerErrorLog =
createOpts?.handleServerErrorLog ||
((e) => {
console.error("Action error:", (e as Error).message);
});

// If `handleReturnedServerError` is provided, use it to handle server error
// messages returned on the client.
// Otherwise mask the error and use a generic message.
const handleReturnedServerError = (e: Error) =>
createOpts?.handleReturnedServerError?.(e) || DEFAULT_SERVER_ERROR;

// `actionBuilder` is the server function that creates a new action.
// It expects an input schema and a `serverCode` function, so the action
// knows what to do on the server when called by the client.
// It returns a function callable by the client.
const actionBuilder = <const S extends Schema, const Data>(
schema: S,
serverCode: ServerCodeFn<S, Data, Context>
): SafeAction<S, Data> => {
// This is the function called by client. If `input` fails the schema
// parsing, the function will return a `validationError` object, containing
// all the invalid fields provided.
return async (clientInput) => {
try {
const parsedInput = await wrap(schema).validate(clientInput);

// If schema validation fails.
if ("issues" in parsedInput) {
return {
validationErrors: buildValidationErrors(parsedInput.issues),
};
}

// Get the context if `middleware` is provided.
const ctx = (await Promise.resolve(createOpts?.middleware?.(parsedInput.data))) as Context;

// Get `result.data` from the server code function. If it doesn't return
// anything, `data` will be `null`.
const data = ((await serverCode(parsedInput.data, ctx)) ?? null) as Data;

return { data };
} catch (e: unknown) {
// next/navigation functions work by throwing an error that will be
// processed internally by Next.js. So, in this case we need to rethrow it.
if (isRedirectError(e) || isNotFoundError(e)) {
throw e;
}

// If error cannot be handled, warn the user and return a generic message.
if (!isError(e)) {
console.warn("Could not handle server error. Not an instance of Error: ", e);
return { serverError: DEFAULT_SERVER_ERROR };
}

await Promise.resolve(handleServerErrorLog(e));

return {
serverError: await Promise.resolve(handleReturnedServerError(e)),
};
}
};
};

return actionBuilder;
};
87 changes: 0 additions & 87 deletions packages/next-safe-action/src/server.ts

This file was deleted.

53 changes: 0 additions & 53 deletions packages/next-safe-action/src/types.ts

This file was deleted.

3 changes: 2 additions & 1 deletion packages/next-safe-action/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { Infer, Schema, ValidationIssue } from "@decs/typeschema";

export const isError = (error: any): error is Error => error instanceof Error;
export const DEFAULT_SERVER_ERROR = "Something went wrong while executing the operation";
export type MaybePromise<T> = Promise<T> | T;

// This function is used to build the validation errors object from a list of validation issues.
export const buildValidationErrors = <const S extends Schema>(issues: ValidationIssue[]) => {
const validationErrors = {} as Partial<Record<keyof Infer<S> | "_root", string[]>>;

Expand All @@ -22,6 +22,7 @@ export const buildValidationErrors = <const S extends Schema>(issues: Validation
for (const path of paths) {
appendIssue(path, issue.message);
}
// If path is not defined, it means that the issue belongs to the root (global) path.
} else {
appendIssue("_root", issue.message);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/next-safe-action/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineConfig } from "tsup";

export default defineConfig({
entry: ["src/index.ts", "src/hooks.ts", "src/types.ts"],
entry: ["src/index.ts", "src/hooks.ts"],
format: ["esm"],
clean: true,
splitting: false,
Expand Down

0 comments on commit 49d229f

Please sign in to comment.