pingdotgg / zact

Nothing to see here
https://zact-example.vercel.app
MIT License
983 stars 16 forks source link

Introducing zact.input(...).mutation(...)? #20

Open Quatton opened 1 year ago

Quatton commented 1 year ago

Hello. I hope we are familiar with tRPC's router definition pattern where we use method chaining like this. I personally love the idea and strongly believe that it is more intuitive than zact(...)((input) => {})

As this is a breaking change that will introduce new abstraction, I would like to discuss it first before starting work on it.

Firstly, other than the fact that it looks more intuitive, would this solution help us achieve anything else? Secondly, would this cause any problems that we should be aware of?

VictorCalderon commented 1 year ago

I personally love this idea. I have been looking into tRPC's implementation of this "builder API" thing @t3dotgg usually mentions, as it is truly a much easier API to wrap your head around.

As for this:

As this is a breaking change that will introduce new abstraction, I would like to discuss it first before starting work on it.

You could simple add it on top of the existing API, or even as a ZactBuilder thing. I would love to get my hands dirty with this but I am very new to typescript and OSS in general, and I am not even a SWE. I'll see if I can come up with something, or if someone is already working on it, we would love to know.

chungweileong94 commented 1 year ago

I have tried to build this pattern myself, and here's what I learned:

Anyway, here's code snippet if you are interested. The type is heavily inspired by trpc codebase😉

const unsetMarker = Symbol('unsetMarker');
type UnsetMarker = typeof unsetMarker;

type InferParserType<TParser, TType extends 'in' | 'out'> = TParser extends UnsetMarker
  ? undefined
  : TParser extends z.ZodType
  ? TParser[TType extends 'in' ? '_input' : '_output']
  : never;

type ActionParams<TInput = unknown> = {
  _input: TInput;
};

type ActionBuilder<TParams extends ActionParams> = {
  input: <TParser extends z.ZodType>(input: TParser) => ActionBuilder<{_input: TParser}>;
  action: <TOutput>(
    action: (input: InferParserType<TParams['_input'], 'out'>) => Promise<TOutput>,
  ) => (input: InferParserType<TParams['_input'], 'in'>) => Promise<TOutput>;
};
type AnyActionBuilder = ActionBuilder<any>;

type ActionBuilderDef<TParams extends ActionParams<any>> = {
  input: TParams['_input'];
};
type AnyActionBuilderDef = ActionBuilderDef<any>;

const createNewServerActionBuilder = (def: Partial<AnyActionBuilderDef>) => {
  return createServerActionBuilder(def);
};

const createServerActionBuilder = (
  initDef: Partial<AnyActionBuilderDef> = {},
): ActionBuilder<{
  _input: UnsetMarker;
}> => {
  const _def: ActionBuilderDef<{_input: z.ZodType | undefined}> = {
    input: undefined,
    ...initDef,
  };
  return {
    input: (input) => createNewServerActionBuilder({..._def, input}) as AnyActionBuilder,
    action: (action) => {
      return async (input) => {
        if (_def.input) {
          const result = _def.input.safeParse(input);
          if (!result.success) {
            throw fromZodError(result.error);
          }
        }
        return await action(input);
      };
    },
  };
};

export const zact = createServerActionBuilder();

// Usages
const action1 = zact.action(async () => {});
const action2 = zact.input(z.string()).action(async (id) => {});
Quatton commented 1 year ago

Wow, this is exactly what I mentioned.

It might be overkill if we don't have any additional functionality, like .output(...) etc.

Not an overkill IMO. We can definitely make use of a middleware like "zact.use()"

KATT commented 1 year ago

FYI, we're working on this with tRPC: https://github.com/trpc/examples-next-app-dir/blob/main/src/app/server-action/_actions.tsx

If you want to help us improve, join our Discord and chat in one of the contributor channels :)

Quatton commented 1 year ago

 @KATT That's amazing! Will do!

chungweileong94 commented 1 year ago

FYI, we're working on this with tRPC: https://github.com/trpc/examples-next-app-dir/blob/main/src/app/server-action/_actions.tsx

This is really cool! Will try my best to find a way to help out you guys🙂

I'm currently building a really simple server action builder for my personal use that looks similar to tRPC builder pattern. If you are interested in building something similar, feel free to take a look.