Skip to content

Commit

Permalink
Add new fields to API route context (#4986)
Browse files Browse the repository at this point in the history
* Add new fields to API route context

* Add props and redirect

* Pass custom redirect status code

* Correctly pass props in api routes

* Add better docs

* Add test

* Fix build

* Add constants file

* Add links to jsdoc

* Workaround lint issue

* Thanks Chris

Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>

* Missed one doc change

Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>

* Add more detail to the changesets

* Strict redirect status type

Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
Co-authored-by: Matthew Phillips <matthew@skypack.dev>
  • Loading branch information
3 people authored Oct 12, 2022
1 parent 07d16ff commit ebd364e
Show file tree
Hide file tree
Showing 17 changed files with 257 additions and 37 deletions.
19 changes: 19 additions & 0 deletions .changeset/fifty-ads-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'astro': minor
---

## New properties for API routes

In API routes, you can now get the `site`, `generator`, `url`, `clientAddress`, `props`, and `redirect` fields on the APIContext, which is the first parameter passed to an API route. This was done to make the APIContext more closely align with the `Astro` global in .astro pages.

For example, here's how you might use the `clientAddress`, which is the user's IP address, to selectively allow users.

```js
export function post({ clientAddress, request, redirect }) {
if(!allowList.has(clientAddress)) {
return redirect('/not-allowed');
}
}
```

Check out the docs for more information on the newly available fields: https://docs.astro.build/en/core-concepts/endpoints/#server-endpoints-api-routes
14 changes: 14 additions & 0 deletions .changeset/selfish-foxes-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'astro': minor
---

## Support passing a custom status code for Astro.redirect

New in this minor is the ability to pass a status code to `Astro.redirect`. By default it uses `302` but now you can pass another code as the second argument:

```astro
---
// This page was moved
return Astro.redirect('/posts/new-post-name', 301);
---
```
130 changes: 111 additions & 19 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ export interface BuildConfig {
* [Astro reference](https://docs.astro.build/reference/api-reference/#astro-global)
*/
export interface AstroGlobal<Props extends Record<string, any> = Record<string, any>>
extends AstroGlobalPartial {
extends AstroGlobalPartial,
AstroSharedContext<Props> {
/**
* Canonical URL of the current page.
* @deprecated Use `Astro.url` instead.
Expand All @@ -107,21 +108,13 @@ export interface AstroGlobal<Props extends Record<string, any> = Record<string,
* ```
*/
canonicalURL: URL;
/** The address (usually IP address) of the user. Used with SSR only.
*
*/
clientAddress: string;
/**
* A full URL object of the request URL.
* Equivalent to: `new URL(Astro.request.url)`
*
* [Astro reference](https://docs.astro.build/en/reference/api-reference/#url)
*/
/**
* Utility for getting and setting cookies values.
*/
cookies: AstroCookies;
url: URL;
url: AstroSharedContext['url'];
/** Parameters passed to a dynamic page generated using [getStaticPaths](https://docs.astro.build/en/reference/api-reference/#getstaticpaths)
*
* Example usage:
Expand All @@ -138,9 +131,9 @@ export interface AstroGlobal<Props extends Record<string, any> = Record<string,
* <h1>{id}</h1>
* ```
*
* [Astro reference](https://docs.astro.build/en/reference/api-reference/#params)
* [Astro reference](https://docs.astro.build/en/reference/api-reference/#astroparams)
*/
params: Params;
params: AstroSharedContext['params'];
/** List of props passed to this component
*
* A common way to get specific props is through [destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment), ex:
Expand All @@ -150,7 +143,7 @@ export interface AstroGlobal<Props extends Record<string, any> = Record<string,
*
* [Astro reference](https://docs.astro.build/en/core-concepts/astro-components/#component-props)
*/
props: Props;
props: AstroSharedContext<Props>['props'];
/** Information about the current request. This is a standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object
*
* For example, to get a URL object of the current URL, you can use:
Expand Down Expand Up @@ -184,11 +177,11 @@ export interface AstroGlobal<Props extends Record<string, any> = Record<string,
*
* [Astro reference](https://docs.astro.build/en/guides/server-side-rendering/#astroredirect)
*/
redirect(path: string): Response;
redirect: AstroSharedContext['redirect'];
/**
* The <Astro.self /> element allows a component to reference itself recursively.
*
* [Astro reference](https://docs.astro.build/en/guides/server-side-rendering/#astroself)
* [Astro reference](https://docs.astro.build/en/guides/api-reference/#astroself)
*/
self: AstroComponentFactory;
/** Utility functions for modifying an Astro component’s slotted children
Expand Down Expand Up @@ -1077,8 +1070,6 @@ export type PaginateFunction = (data: any[], args?: PaginateOptions) => GetStati

export type Params = Record<string, string | number | undefined>;

export type Props = Record<string, unknown>;

export interface AstroAdapter {
name: string;
serverEntrypoint?: string;
Expand All @@ -1088,12 +1079,113 @@ export interface AstroAdapter {

type Body = string;

export interface APIContext {
// Shared types between `Astro` global and API context object
interface AstroSharedContext<Props extends Record<string, any> = Record<string, any>> {
/**
* The address (usually IP address) of the user. Used with SSR only.
*/
clientAddress: string;
/**
* Utility for getting and setting the values of cookies.
*/
cookies: AstroCookies;
params: Params;
/**
* Information about the current request. This is a standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object
*/
request: Request;
/**
* A full URL object of the request URL.
*/
url: URL;
/**
* Route parameters for this request if this is a dynamic route.
*/
params: Params;
/**
* List of props returned for this path by `getStaticPaths` (**Static Only**).
*/
props: Props;
/**
* Redirect to another page (**SSR Only**).
*/
redirect(path: string, status?: 301 | 302 | 308): Response;
}

export interface APIContext<Props extends Record<string, any> = Record<string, any>>
extends AstroSharedContext<Props> {
site: URL | undefined;
generator: string;
/**
* A full URL object of the request URL.
* Equivalent to: `new URL(request.url)`
*/
url: AstroSharedContext['url'];
/**
* Parameters matching the page’s dynamic route pattern.
* In static builds, this will be the `params` generated by `getStaticPaths`.
* In SSR builds, this can be any path segments matching the dynamic route pattern.
*
* Example usage:
* ```ts
* export function getStaticPaths() {
* return [
* { params: { id: '0' }, props: { name: 'Sarah' } },
* { params: { id: '1' }, props: { name: 'Chris' } },
* { params: { id: '2' }, props: { name: 'Fuzzy' } },
* ];
* }
*
* export async function get({ params }) {
* return {
* body: `Hello user ${params.id}!`,
* }
* }
* ```
*
* [context reference](https://docs.astro.build/en/guides/api-reference/#contextparams)
*/
params: AstroSharedContext['params'];
/**
* List of props passed from `getStaticPaths`. Only available to static builds.
*
* Example usage:
* ```ts
* export function getStaticPaths() {
* return [
* { params: { id: '0' }, props: { name: 'Sarah' } },
* { params: { id: '1' }, props: { name: 'Chris' } },
* { params: { id: '2' }, props: { name: 'Fuzzy' } },
* ];
* }
*
* export function get({ props }) {
* return {
* body: `Hello ${props.name}!`,
* }
* }
* ```
*
* [context reference](https://docs.astro.build/en/guides/api-reference/#contextprops)
*/
props: AstroSharedContext<Props>['props'];
/**
* Redirect to another page. Only available in SSR builds.
*
* Example usage:
* ```ts
* // src/pages/secret.ts
* export function get({ redirect }) {
* return redirect('/login');
* }
* ```
*
* [context reference](https://docs.astro.build/en/guides/api-reference/#contextredirect)
*/
redirect: AstroSharedContext['redirect'];
}

export type Props = Record<string, unknown>;

export interface EndpointOutput {
body: Body;
encoding?: BufferEncoding;
Expand Down
3 changes: 2 additions & 1 deletion packages/astro/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import { enableVerboseLogging, nodeLogDestination } from '../core/logger/node.js
import { formatConfigErrorMessage, formatErrorMessage, printHelp } from '../core/messages.js';
import { appendForwardSlash } from '../core/path.js';
import preview from '../core/preview/index.js';
import { ASTRO_VERSION, createSafeError } from '../core/util.js';
import { ASTRO_VERSION } from '../core/constants.js';
import { createSafeError } from '../core/util.js';
import * as event from '../events/index.js';
import { eventConfigError, eventError, telemetry } from '../events/index.js';
import { check } from './check/index.js';
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/core/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// process.env.PACKAGE_VERSION is injected when we build and publish the astro package.
export const ASTRO_VERSION = process.env.PACKAGE_VERSION ?? 'development';
2 changes: 2 additions & 0 deletions packages/astro/src/core/endpoint/dev/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export async function call(ssrOpts: SSROptions) {
return await callEndpoint(mod as unknown as EndpointHandler, {
...ssrOpts,
ssr: ssrOpts.settings.config.output === 'server',
site: ssrOpts.settings.config.site,
adapterName: ssrOpts.settings.config.adapter?.name,
});
}
55 changes: 52 additions & 3 deletions packages/astro/src/core/endpoint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import type { RenderOptions } from '../render/core';
import { renderEndpoint } from '../../runtime/server/index.js';
import { AstroCookies, attachToResponse } from '../cookies/index.js';
import { getParamsAndProps, GetParamsAndPropsError } from '../render/core.js';
import { ASTRO_VERSION } from '../constants.js';

const clientAddressSymbol = Symbol.for('astro.clientAddress');

export type EndpointOptions = Pick<
RenderOptions,
Expand All @@ -17,6 +20,7 @@ export type EndpointOptions = Pick<
| 'site'
| 'ssr'
| 'status'
| 'adapterName'
>;

type EndpointCallResult =
Expand All @@ -30,11 +34,50 @@ type EndpointCallResult =
response: Response;
};

function createAPIContext(request: Request, params: Params): APIContext {
function createAPIContext({
request,
params,
site,
props,
adapterName,
}: {
request: Request;
params: Params;
site?: string;
props: Record<string, any>;
adapterName?: string;
}): APIContext {
return {
cookies: new AstroCookies(request),
request,
params,
site: site ? new URL(site) : undefined,
generator: `Astro v${ASTRO_VERSION}`,
props,
redirect(path, status) {
return new Response(null, {
status: status || 302,
headers: {
Location: path,
},
});
},
url: new URL(request.url),
get clientAddress() {
if (!(clientAddressSymbol in request)) {
if (adapterName) {
throw new Error(
`clientAddress is not available in the ${adapterName} adapter. File an issue with the adapter to add support.`
);
} else {
throw new Error(
`clientAddress is not available in your environment. Ensure that you are using an SSR adapter that supports this feature.`
);
}
}

return Reflect.get(request, clientAddressSymbol);
},
};
}

Expand All @@ -49,9 +92,15 @@ export async function call(
`[getStaticPath] route pattern matched, but no matching static path found. (${opts.pathname})`
);
}
const [params] = paramsAndPropsResp;
const [params, props] = paramsAndPropsResp;

const context = createAPIContext(opts.request, params);
const context = createAPIContext({
request: opts.request,
params,
props,
site: opts.site,
adapterName: opts.adapterName,
});
const response = await renderEndpoint(mod, context, opts.ssr);

if (response instanceof Response) {
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/render/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export async function getParamsAndProps(
}

export interface RenderOptions {
adapterName: string | undefined;
adapterName?: string;
logging: LogOptions;
links: Set<SSRElement>;
styles?: Set<SSRElement>;
Expand Down
13 changes: 7 additions & 6 deletions packages/astro/src/core/render/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,8 @@ export function createResult(args: CreateResultArgs): SSRResult {
) {
const astroSlots = new Slots(result, slots, args.logging);

const Astro = {
const Astro: AstroGlobal = {
// @ts-expect-error set prototype
__proto__: astroGlobal,
get clientAddress() {
if (!(clientAddressSymbol in request)) {
Expand Down Expand Up @@ -196,9 +197,9 @@ export function createResult(args: CreateResultArgs): SSRResult {
request,
url,
redirect: args.ssr
? (path: string) => {
? (path, status) => {
return new Response(null, {
status: 302,
status: status || 302,
headers: {
Location: path,
},
Expand Down Expand Up @@ -237,9 +238,9 @@ ${extra}`
// Intentionally return an empty string so that it is not relied upon.
return '';
},
response,
slots: astroSlots,
} as unknown as AstroGlobal;
response: response as AstroGlobal['response'],
slots: astroSlots as unknown as AstroGlobal['slots'],
};

Object.defineProperty(Astro, 'canonicalURL', {
get: function () {
Expand Down
3 changes: 0 additions & 3 deletions packages/astro/src/core/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ import type { ErrorPayload, ViteDevServer } from 'vite';
import type { AstroConfig, AstroSettings, RouteType } from '../@types/astro';
import { prependForwardSlash, removeTrailingForwardSlash } from './path.js';

// process.env.PACKAGE_VERSION is injected when we build and publish the astro package.
export const ASTRO_VERSION = process.env.PACKAGE_VERSION ?? 'development';

/** Returns true if argument is an object of any prototype/class (but not null). */
export function isObject(value: unknown): value is Record<string, any> {
return typeof value === 'object' && value != null;
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/events/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AstroTelemetry } from '@astrojs/telemetry';
import { createRequire } from 'module';
import { ASTRO_VERSION } from '../core/util.js';
import { ASTRO_VERSION } from '../core/constants.js';
const require = createRequire(import.meta.url);

function getViteVersion() {
Expand Down
Loading

0 comments on commit ebd364e

Please sign in to comment.