0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-23 21:53:55 -05:00
astro/.changeset/shaggy-moons-peel.md
Ben Holmes c0c509b6bf
Actions experimental release (#10858)
* feat: port astro-actions poc

* feat: basic blog example

* feat: basic validationError class

* feat: standard error types and safe() wrapper

* refactor: move enhanceProps to astro:actions

* fix: throw internal server errors

* chore: refine enhance: true error message

* fix: remove FormData fallback from route

* refactor: clarify what enhance: true allows

* feat: progressively enhanced comments

* chore: changeset

* refactor: enhance -> acceptFormData

* wip: migrate actions to core

* feat: working actions demo from astro core!

* chore: changeset

* chore: delete old changeset

* fix: Function type lint

* refactor: expose defineAction from `astro:actions`

* fix: add null check to experimental

* fix: export `types/actions.d.ts`

* feat: more robust form data parsing

* feat: support formData from rpc call

* feat: remove acceptFormData flag requirement

* feat: add actions.d.ts type reference on startup

* refactor: actionNameProps -> getNameProps

* fix: actions type import

* chore: expose zod from `astro:actions`

* fix: zod export path

* feat: add explicit `accept` property

* Use zod package instead of relative path outside of src

* feat: clean up error throwing and handling flow

* fix: make `accept` optional

* docs: beef up actions experimental docs

* fix: defineAction type narrowing on `accept`

* fix: bad `getNameProps()` arg type

* refactor: move to single `error` object + `isInputError()` util

* fix: move res.json() parse to avoid double parse

* feat: support async zod schemas

* feat: serialize and expose zod properties on input error

* feat: test input error in comment example

* fix: remove ZodError import

* fix: add actions-module to files export

* fix: use workspace for test pkg versions

* refactor: default export -> server export

* fix: type inference for json vs. form

* refactor: accept form -> defineFormAction

* refactor: better callSafely signature

* feat: block action calls from the server with RFC link

* feat: move getActionResult to global

* refactor: getNameProps -> getActionProps

* refactor: body.toString()

* edit: capitAl

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* edit: highlight `actions`

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* edit: add actions file name

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* edit: not you can. You DO

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* edit: declare with feeling

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* edit: clarify what the `handler` does

* edit: schema -> input

* edit: add FormData mdn reference

* edit: add defineFormAction() explainer

* refactor: inline getDotAstroTypeRefs

* edit: yeah yeah maybe

* fix: existsSync test mock

* refactor: use callSafely in middleware

* test: upgradeFormData()

* chore: stray console log

* refactor: extract helper functions

* fix: include status in error response

* fix: return `undefined` when there's no action result

* fix: content-type

* test: e2e like button action

* test: comment e2e

* fix: existsSync mock for other sync test

* test: action dev server raw fetch

* test: build preview

* chore: fix lock

* fix: add dotAstroDir to existsSync

* chore: slim down e2e fixture

* chore: remove unneeded disabled test

* refactor: better api context error

* fix: return `false` for envDts

* refactor: defineFormAction -> defineAction with accept

* fix: check FormData on getActionProps

* edit: uppercase

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* fix: add switch default for 500

Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>

* fix: add `toLowerCase()` on content-type check

Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>

* chore: use VIRTUAL_MODULE_ID for plugin

* fix: remove incorrect ts-ignore

* chore: remove unneeded POST method check

* refactor: route callSafely

* refactor: error switch case to map

* chore: add link to trpc error code table

* fix: add readable error on failed json.stringify

* refactor: add param -> callerParam with comment

* feat: always return safe from getActionResult()

* refactor: move actions module to templates/

* refactor: remove unneeded existsSync on dotAstro

* fix: hasContentType util for toLowerCase()

* chore: comment on 415 code

* refactor: upgradeFormData -> formDataToObj

* fix: avoid leaking stack in production

* refactor: defineProperty with write false

* fix: revert package.json back to spaces

* edit: use config docs for changeset

* refactor: stringifiedActionsPath -> stringifiedActionsImport

* fix: avoid double-handling for route

* fix: support zero arg actions

* refactor: move actionHandler to helper fn

* fix: restore mdast deps

* docs: add `output` to config

---------

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
Co-authored-by: bholmesdev <bholmesdev@gmail.com>
2024-05-08 07:53:17 -04:00

3.2 KiB

astro
minor

Adds experimental support for the Actions API. Actions let you define type-safe endpoints you can query from client components with progressive enhancement built in.

Actions help you write type-safe backend functions you can call from anywhere. Enable server rendering using the output property and add the actions flag to the experimental object:

{
  output: 'hybrid', // or 'server'
  experimental: {
    actions: true,
  },
}

Declare all your actions in src/actions/index.ts. This file is the global actions handler.

Define an action using the defineAction() utility from the astro:actions module. These accept the handler property to define your server-side request handler. If your action accepts arguments, apply the input property to validate parameters with Zod.

This example defines two actions: like and comment. The like action accepts a JSON object with a postId string, while the comment action accepts FormData with postId, author, and body strings. Each handler updates your database and return a type-safe response.

// src/actions/index.ts
import { defineAction, z } from "astro:actions";
	 
export const server = {
  like: defineAction({
    input: z.object({ postId: z.string() }),
    handler: async ({ postId }, context) => {
      // update likes in db
	 
      return likes;
    },
  }),
  comment: defineAction({
    accept: 'form',
    input: z.object({
      postId: z.string(),
      author: z.string(),
      body: z.string(),
    }),
    handler: async ({ postId }, context) => {
      // insert comments in db

      return comment;
    },
  }),
};

Then, call an action from your client components using the actions object from astro:actions. You can pass a type-safe object when using JSON, or a FormData object when using accept: 'form' in your action definition:

// src/components/blog.tsx
import { actions } from "astro:actions";
import { useState } from "preact/hooks";
	 
export function Like({ postId }: { postId: string }) {
  const [likes, setLikes] = useState(0);
  return (
    <button
      onClick={async () => {
        const newLikes = await actions.like({ postId });
        setLikes(newLikes);
      }}
    >
      {likes} likes
    </button>
  );
}
	 
export function Comment({ postId }: { postId: string }) {
  return (
    <form
      onSubmit={async (e) => {
        e.preventDefault();
        const formData = new FormData(e.target);
        const result = await actions.blog.comment(formData);
        // handle result
      }}
    >
      <input type="hidden" name="postId" value={postId} />
      <label for="author">Author</label>
      <input id="author" type="text" name="author" />
      <textarea rows={10} name="body"></textarea>
      <button type="submit">Post</button>
    </form>
  );
}

For a complete overview, and to give feedback on this experimental API, see the Actions RFC.