0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-16 21:46:22 -05:00

Actions: expand isInputError to accept unknown (#11439)

* feat: allow type `unknown` on `isInputError`

* chore: move ErrorInferenceObject to internal utils

* chore: changeset

* deps: expect-type

* feat: first types test

* chore: add types test to general test command

* refactor: use describe and it for organization
This commit is contained in:
Ben Holmes 2024-07-09 15:54:49 -04:00 committed by GitHub
parent ea8582f4fc
commit 08baf56f32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 90 additions and 12 deletions

View file

@ -0,0 +1,18 @@
---
'astro': patch
---
Expands the `isInputError()` utility from `astro:actions` to accept errors of any type. This should now allow type narrowing from a try / catch block.
```ts
// example.ts
import { actions, isInputError } from 'astro:actions';
try {
await actions.like(new FormData());
} catch (error) {
if (isInputError(error)) {
console.log(error.fields);
}
}
```

View file

@ -24,6 +24,7 @@
"test:citgm": "pnpm -r --filter=astro test",
"test:match": "cd packages/astro && pnpm run test:match",
"test:unit": "cd packages/astro && pnpm run test:unit",
"test:types": "cd packages/astro && pnpm run test:types",
"test:unit:match": "cd packages/astro && pnpm run test:unit:match",
"test:smoke": "pnpm test:smoke:example && pnpm test:smoke:docs",
"test:smoke:example": "turbo run build --concurrency=100% --filter=\"@example/*\"",

View file

@ -114,12 +114,13 @@
"build:ci": "pnpm run prebuild && astro-scripts build \"src/**/*.{ts,js}\" && pnpm run postbuild",
"dev": "astro-scripts dev --copy-wasm --prebuild \"src/runtime/server/astro-island.ts\" --prebuild \"src/runtime/client/{idle,load,media,only,visible}.ts\" \"src/**/*.{ts,js}\"",
"postbuild": "astro-scripts copy \"src/**/*.astro\" && astro-scripts copy \"src/**/*.wasm\"",
"test": "pnpm run test:node",
"test": "pnpm run test:node && pnpm run test:types",
"test:match": "pnpm run test:node --match",
"test:e2e": "pnpm test:e2e:chrome && pnpm test:e2e:firefox",
"test:e2e:match": "playwright test -g",
"test:e2e:chrome": "playwright test",
"test:e2e:firefox": "playwright test --config playwright.firefox.config.js",
"test:types": "tsc --project tsconfig.tests.json",
"test:node": "astro-scripts test \"test/**/*.test.js\""
},
"dependencies": {
@ -215,6 +216,7 @@
"astro-scripts": "workspace:*",
"cheerio": "1.0.0-rc.12",
"eol": "^0.9.1",
"expect-type": "^0.19.0",
"mdast-util-mdx": "^3.0.0",
"mdast-util-mdx-jsx": "^3.1.2",
"memfs": "^4.9.3",

View file

@ -31,3 +31,14 @@ export async function getAction(
}
return actionLookup;
}
/**
* Used to preserve the input schema type in the error object.
* This allows for type inference on the `fields` property
* when type narrowed to an `ActionInputError`.
*
* Example: Action has an input schema of `{ name: z.string() }`.
* When calling the action and checking `isInputError(result.error)`,
* `result.error.fields` will be typed with the `name` field.
*/
export type ErrorInferenceObject = Record<string, any>;

View file

@ -1,13 +1,7 @@
import { z } from 'zod';
import { type ActionAPIContext, getApiContext as _getApiContext } from '../store.js';
import { type MaybePromise } from '../utils.js';
import {
ActionError,
ActionInputError,
type ErrorInferenceObject,
type SafeResult,
callSafely,
} from './shared.js';
import type { ErrorInferenceObject, MaybePromise } from '../utils.js';
import { ActionError, ActionInputError, type SafeResult, callSafely } from './shared.js';
export * from './shared.js';

View file

@ -1,5 +1,5 @@
import type { z } from 'zod';
import type { MaybePromise } from '../utils.js';
import type { ErrorInferenceObject, MaybePromise } from '../utils.js';
type ActionErrorCode =
| 'BAD_REQUEST'
@ -40,8 +40,6 @@ const statusToCodeMap: Record<number, ActionErrorCode> = Object.entries(codeToSt
{}
);
export type ErrorInferenceObject = Record<string, any>;
export class ActionError<T extends ErrorInferenceObject = ErrorInferenceObject> extends Error {
type = 'AstroActionError';
code: ActionErrorCode = 'INTERNAL_SERVER_ERROR';
@ -85,6 +83,10 @@ export class ActionError<T extends ErrorInferenceObject = ErrorInferenceObject>
export function isInputError<T extends ErrorInferenceObject>(
error?: ActionError<T>
): error is ActionInputError<T>;
export function isInputError(error?: unknown): error is ActionInputError<ErrorInferenceObject>;
export function isInputError<T extends ErrorInferenceObject>(
error?: unknown | ActionError<T>
): error is ActionInputError<T> {
return error instanceof ActionInputError;
}

View file

@ -0,0 +1,31 @@
import { expectTypeOf } from 'expect-type';
import { isInputError, defineAction } from '../../dist/actions/runtime/virtual/server.js';
import { z } from '../../zod.mjs';
import { describe, it } from 'node:test';
const exampleAction = defineAction({
input: z.object({
name: z.string(),
}),
handler: () => {},
});
const result = await exampleAction.safe({ name: 'Alice' });
describe('isInputError', () => {
it('isInputError narrows unknown error types', async () => {
try {
await exampleAction({ name: 'Alice' });
} catch (e) {
if (isInputError(e)) {
expectTypeOf(e.fields).toEqualTypeOf<Record<string, string[] | undefined>>();
}
}
});
it('`isInputError` preserves `fields` object type for ActionError objects', async () => {
if (isInputError(result.error)) {
expectTypeOf(result.error.fields).toEqualTypeOf<{ name?: string[] }>();
}
});
});

View file

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"include": ["test/types"],
"compilerOptions": {
"allowJs": true,
"emitDeclarationOnly": false,
"noEmit": true,
}
}

View file

@ -794,6 +794,9 @@ importers:
eol:
specifier: ^0.9.1
version: 0.9.1
expect-type:
specifier: ^0.19.0
version: 0.19.0
mdast-util-mdx:
specifier: ^3.0.0
version: 3.0.0
@ -8654,6 +8657,10 @@ packages:
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
engines: {node: '>=16.17'}
expect-type@0.19.0:
resolution: {integrity: sha512-piv9wz3IrAG4Wnk2A+n2VRCHieAyOSxrRLU872Xo6nyn39kYXKDALk4OcqnvLRnFvkz659CnWC8MWZLuuQnoqg==}
engines: {node: '>=12.0.0'}
express@4.19.2:
resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==}
engines: {node: '>= 0.10.0'}
@ -9308,6 +9315,7 @@ packages:
libsql@0.3.12:
resolution: {integrity: sha512-to30hj8O3DjS97wpbKN6ERZ8k66MN1IaOfFLR6oHqd25GMiPJ/ZX0VaZ7w+TsPmxcFS3p71qArj/hiedCyvXCg==}
cpu: [x64, arm64, wasm32]
os: [darwin, linux, win32]
lilconfig@2.1.0:
@ -14704,6 +14712,8 @@ snapshots:
signal-exit: 4.1.0
strip-final-newline: 3.0.0
expect-type@0.19.0: {}
express@4.19.2:
dependencies:
accepts: 1.3.8