Skip to content

Commit

Permalink
feat: support setting http response status in functions
Browse files Browse the repository at this point in the history
  • Loading branch information
jonbretman committed Nov 17, 2023
1 parent 481f2a8 commit 0283907
Show file tree
Hide file tree
Showing 18 changed files with 151 additions and 39 deletions.
5 changes: 3 additions & 2 deletions functions/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ type FunctionsRuntimeResponse struct {

type FunctionsRuntimeMeta struct {
Headers map[string][]string `json:"headers"`
Status int `json:"status"`
}

// FunctionsRuntimeError follows the error object specification
Expand All @@ -91,7 +92,7 @@ func WithFunctionsTransport(ctx context.Context, transport Transport) context.Co
}

// CallFunction will invoke the custom function on the runtime node server.
func CallFunction(ctx context.Context, actionName string, body any, permissionState *common.PermissionState) (any, map[string][]string, error) {
func CallFunction(ctx context.Context, actionName string, body any, permissionState *common.PermissionState) (any, *FunctionsRuntimeMeta, error) {
span := trace.SpanFromContext(ctx)

transport, ok := ctx.Value(contextKey).(Transport)
Expand Down Expand Up @@ -156,7 +157,7 @@ func CallFunction(ctx context.Context, actionName string, body any, permissionSt
return nil, nil, toRuntimeError(resp.Error)
}

return resp.Result, resp.Meta.Headers, nil
return resp.Result, resp.Meta, nil
}

// CallJob will invoke the job function on the runtime node server.
Expand Down
10 changes: 10 additions & 0 deletions integration/testdata/functions_http/functions/withHeaders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { WithHeaders, permissions } from "@teamkeel/sdk";

export default WithHeaders(async (ctx, inputs) => {
permissions.allow();

const value = ctx.headers.get("X-MyRequestHeader");
ctx.response.headers.set("X-MyResponseHeader", value || "");

return {};
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { WithQueryParams, permissions } from "@teamkeel/sdk";

export default WithQueryParams(async (ctx, inputs) => {
permissions.allow();

return inputs;
});
10 changes: 10 additions & 0 deletions integration/testdata/functions_http/functions/withStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { WithStatus, permissions } from "@teamkeel/sdk";
export default WithStatus(async (ctx, inputs) => {
permissions.allow();

const { response } = ctx;
response.headers.set("Location", "https://some.url");
response.status = 301;

return null;
});
7 changes: 7 additions & 0 deletions integration/testdata/functions_http/schema.keel
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
model TestModel {
actions {
read withQueryParams(Any) returns (Any)
read withHeaders(Any) returns (Any)
read withStatus(Any) returns (Any)
}
}
50 changes: 50 additions & 0 deletions integration/testdata/functions_http/tests.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { test, expect } from "vitest";

test("headers", async () => {
const response = await fetch(
process.env.KEEL_TESTING_ACTIONS_API_URL + "/withHeaders",
{
body: "{}",
method: "POST",
headers: {
"Content-Type": "application/json",
"X-MyRequestHeader": "my-header-value",
},
}
);

expect(response.status).toEqual(200);
expect(response.headers.get("X-MyResponseHeader")).toEqual("my-header-value");
});

test("status", async () => {
const response = await fetch(
process.env.KEEL_TESTING_ACTIONS_API_URL + "/withStatus",
{
body: "{}",
method: "POST",
headers: {
"Content-Type": "application/json",
},
redirect: "manual",
}
);

expect(response.status).toEqual(301);
expect(response.headers.get("Location")).toEqual("https://some.url");
});

test("query params", async () => {
const response = await fetch(
process.env.KEEL_TESTING_ACTIONS_API_URL +
"/withQueryParams?a=1&b=foo&c=true"
);

const body = await response.json();

expect(body).toEqual({
a: "1",
b: "foo",
c: "true",
});
});
2 changes: 1 addition & 1 deletion node/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func Bootstrap(dir string, opts ...BootstrapOption) error {
options.logger("Creating tsconfig.json")
tsConfig := map[string]any{
"compilerOptions": map[string]any{
"lib": []string{"ES2016"},
"lib": []string{"ES2016", "DOM"},
"target": "ES2016",
"esModuleInterop": true,
"moduleResolution": "node",
Expand Down
2 changes: 1 addition & 1 deletion node/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@ func writeAPIDeclarations(w *codegen.Writer, schema *proto.Schema) {
func writeAPIFactory(w *codegen.Writer, schema *proto.Schema) {
w.Writeln("function createContextAPI({ responseHeaders, meta }) {")
w.Indent()
w.Writeln("const headers = new runtime.RequestHeaders(meta.headers);")
w.Writeln("const headers = new Headers(meta.headers);")
w.Writeln("const response = { headers: responseHeaders }")
w.Writeln("const now = () => { return new Date(); };")
w.Writeln("const { identity } = meta;")
Expand Down
2 changes: 1 addition & 1 deletion node/codegen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ job BatchPosts {
func TestWriteAPIFactory(t *testing.T) {
expected := `
function createContextAPI({ responseHeaders, meta }) {
const headers = new runtime.RequestHeaders(meta.headers);
const headers = new Headers(meta.headers);
const response = { headers: responseHeaders }
const now = () => { return new Date(); };
const { identity } = meta;
Expand Down
8 changes: 4 additions & 4 deletions packages/functions-runtime/src/RequestHeaders.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ class RequestHeaders {
this._headers = new Headers(requestHeaders);
}

get(key) {
return this._headers.get(key);
get() {
return this._headers.get(...arguments);
}

has(key) {
return this._headers.has(key);
has() {
return this._headers.has(...arguments);
}
}

Expand Down
5 changes: 4 additions & 1 deletion packages/functions-runtime/src/handleRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@ async function handleRequest(request, config) {
for (const pair of headers.entries()) {
responseHeaders[pair[0]] = pair[1].split(", ");
}
response.meta = { headers: responseHeaders };
response.meta = {
headers: responseHeaders,
status: ctx.response.status,
};

return response;
} catch (e) {
Expand Down
6 changes: 2 additions & 4 deletions packages/functions-runtime/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export type ContextAPI = {

export type Response = {
headers: Headers;
status?: number;
};

export type PageInfo = {
Expand All @@ -70,10 +71,7 @@ export type PageInfo = {
};

// Request headers query API
export type RequestHeaders = {
get(name: string): string;
has(name: string): boolean;
};
export type RequestHeaders = Omit<Headers, "append" | "delete" | "set">;

export declare class Permissions {
constructor();
Expand Down
26 changes: 16 additions & 10 deletions runtime/actions/scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package actions
import (
"context"
"fmt"
"net/http"

"github.com/teamkeel/keel/events"
"github.com/teamkeel/keel/functions"
Expand Down Expand Up @@ -82,7 +83,7 @@ func NewJobScope(
}
}

func Execute(scope *Scope, inputs any) (result any, headers map[string][]string, err error) {
func Execute(scope *Scope, inputs any) (result any, meta *common.ResponseMetadata, err error) {
ctx, span := tracer.Start(scope.Context, scope.Action.Name)
defer span.End()

Expand All @@ -99,7 +100,7 @@ func Execute(scope *Scope, inputs any) (result any, headers map[string][]string,

switch scope.Action.Implementation {
case proto.ActionImplementation_ACTION_IMPLEMENTATION_CUSTOM:
result, headers, err = executeCustomFunction(scope, inputs)
result, meta, err = executeCustomFunction(scope, inputs)
case proto.ActionImplementation_ACTION_IMPLEMENTATION_RUNTIME:
if !inputWasAMap {
if inputs == nil {
Expand All @@ -108,7 +109,7 @@ func Execute(scope *Scope, inputs any) (result any, headers map[string][]string,
return nil, nil, fmt.Errorf("inputs %v were not in correct format", inputs)
}
}
result, headers, err = executeRuntimeAction(scope, inputsAsMap)
result, meta, err = executeRuntimeAction(scope, inputsAsMap)
case proto.ActionImplementation_ACTION_IMPLEMENTATION_AUTO:
if !inputWasAMap {
if inputs == nil {
Expand All @@ -117,7 +118,7 @@ func Execute(scope *Scope, inputs any) (result any, headers map[string][]string,
return nil, nil, fmt.Errorf("inputs %v were not in correct format", inputs)
}
}
result, headers, err = executeAutoAction(scope, inputsAsMap)
result, meta, err = executeAutoAction(scope, inputsAsMap)
default:
return nil, nil, fmt.Errorf("unhandled unknown action %s of type %s", scope.Action.Name, scope.Action.Implementation)
}
Expand All @@ -134,7 +135,7 @@ func Execute(scope *Scope, inputs any) (result any, headers map[string][]string,
return
}

func executeCustomFunction(scope *Scope, inputs any) (any, map[string][]string, error) {
func executeCustomFunction(scope *Scope, inputs any) (any, *common.ResponseMetadata, error) {
permissions := proto.PermissionsForAction(scope.Schema, scope.Action)

canAuthoriseEarly, authorised, err := TryResolveAuthorisationEarly(scope, permissions)
Expand All @@ -151,7 +152,7 @@ func executeCustomFunction(scope *Scope, inputs any) (any, map[string][]string,
}
}

resp, headers, err := functions.CallFunction(
resp, meta, err := functions.CallFunction(
scope.Context,
scope.Action.Name,
inputs,
Expand All @@ -162,6 +163,11 @@ func executeCustomFunction(scope *Scope, inputs any) (any, map[string][]string,
return nil, nil, err
}

m := &common.ResponseMetadata{
Headers: http.Header(meta.Headers),
Status: meta.Status,
}

// For now a custom list function just returns a list of records, but the API's
// all return an objects containing results and pagination info. So we need
// to "wrap" the results here.
Expand All @@ -181,13 +187,13 @@ func executeCustomFunction(scope *Scope, inputs any) (any, map[string][]string,
"startCursor": "",
"endCursor": "",
},
}, headers, nil
}, m, nil
}

return resp, headers, err
return resp, m, err
}

func executeRuntimeAction(scope *Scope, inputs map[string]any) (any, map[string][]string, error) {
func executeRuntimeAction(scope *Scope, inputs map[string]any) (any, *common.ResponseMetadata, error) {
switch scope.Action.Name {
case authenticateActionName:
result, err := Authenticate(scope, inputs)
Expand All @@ -203,7 +209,7 @@ func executeRuntimeAction(scope *Scope, inputs map[string]any) (any, map[string]
}
}

func executeAutoAction(scope *Scope, inputs map[string]any) (any, map[string][]string, error) {
func executeAutoAction(scope *Scope, inputs map[string]any) (any, *common.ResponseMetadata, error) {
permissions := proto.PermissionsForAction(scope.Schema, scope.Action)

// Attempt to resolve permissions early; i.e. before row-based database querying.
Expand Down
4 changes: 3 additions & 1 deletion runtime/apis/graphql/graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,9 @@ func NewHandler(s *proto.Schema, api *proto.Api) common.HandlerFunc {
span.SetStatus(codes.Error, strings.Join(messages, ", "))
}

return common.NewJsonResponse(http.StatusOK, result, headers)
return common.NewJsonResponse(http.StatusOK, result, &common.ResponseMetadata{
Headers: headers,
})
}
}

Expand Down
9 changes: 6 additions & 3 deletions runtime/apis/graphql/resolvers.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func ActionFunc(schema *proto.Schema, action *proto.Action) func(p graphql.Resol
scope := actions.NewScope(p.Context, action, schema)
input := p.Args["input"]

res, headers, err := actions.Execute(scope, input)
res, meta, err := actions.Execute(scope, input)
if err != nil {
var runtimeErr common.RuntimeError
if !errors.As(err, &runtimeErr) {
Expand All @@ -31,8 +31,11 @@ func ActionFunc(schema *proto.Schema, action *proto.Action) func(p graphql.Resol

rootValue := p.Info.RootValue.(map[string]interface{})
headersValue := rootValue["headers"].(map[string][]string)
for k, v := range headers {
headersValue[k] = v

if meta != nil {
for k, v := range meta.Headers {
headersValue[k] = v
}
}

if action.Type == proto.ActionType_ACTION_TYPE_LIST {
Expand Down
4 changes: 2 additions & 2 deletions runtime/apis/httpjson/httpjson.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,12 @@ func NewHandler(p *proto.Schema, api *proto.Api) common.HandlerFunc {

scope := actions.NewScope(ctx, action, p)

response, headers, err := actions.Execute(scope, inputs)
response, meta, err := actions.Execute(scope, inputs)
if err != nil {
return NewErrorResponse(ctx, err, nil)
}

return common.NewJsonResponse(http.StatusOK, response, headers)
return common.NewJsonResponse(http.StatusOK, response, meta)
}
}

Expand Down
8 changes: 4 additions & 4 deletions runtime/apis/jsonrpc/jsonrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,12 @@ func NewHandler(p *proto.Schema, api *proto.Api) common.HandlerFunc {

scope := actions.NewScope(ctx, action, p)

response, headers, err := actions.Execute(scope, inputs)
response, meta, err := actions.Execute(scope, inputs)
if err != nil {
return NewErrorResponse(ctx, &req.ID, err)
}

return NewSuccessResponse(ctx, req.ID, response, headers)
return NewSuccessResponse(ctx, req.ID, response, meta)
}
}

Expand Down Expand Up @@ -114,12 +114,12 @@ type JsonRpcError struct {
Detail any `json:"detail,omitempty"`
}

func NewSuccessResponse(ctx context.Context, requestId string, response any, headers map[string][]string) common.Response {
func NewSuccessResponse(ctx context.Context, requestId string, response any, meta *common.ResponseMetadata) common.Response {
return common.NewJsonResponse(http.StatusOK, JsonRpcSuccessResponse{
JsonRpc: "2.0",
ID: requestId,
Result: response,
}, headers)
}, meta)
}

func NewErrorResponse(ctx context.Context, requestId *string, err error) common.Response {
Expand Down
Loading

0 comments on commit 0283907

Please sign in to comment.