Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions src/appendix/cheat-sheet.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Quick reference for Nova's APIs, return values, configuration, and Kura's databa
| `{status, StatusCode}` | Bare status code response |
| `{status, StatusCode, Headers, Body}` | Status with headers and body |
| `{redirect, Path}` | HTTP redirect |
| `{sendfile, StatusCode, Headers, FilePath, Offset, Length}` | Send a file |
| `{sendfile, StatusCode, Headers, {Offset, Length, Path}, MimeType}` | Send a file |

## Route configuration

Expand Down Expand Up @@ -68,7 +68,9 @@ post_request(Req, Env, Options, State) ->
%% Same return values as pre_request

plugin_info() ->
{Title, Version, Author, Description, OptionKeys}.
#{title := binary(), version := binary(), url := binary(),
authors := [binary()], description := binary(),
options => [{atom(), binary()}]}.
```

### Plugin configuration
Expand Down Expand Up @@ -147,9 +149,9 @@ nova_pubsub:get_local_members(Channel)

```erlang
{pre_request, nova_request_plugin, #{
decode_json_body => true, %% Decode JSON request bodies
read_urlencoded_body => true, %% Decode URL-encoded form data
read_body => true %% Read raw body
decode_json_body => true, %% Decode JSON body into `json` key
read_urlencoded_body => true, %% Decode URL-encoded form data into `params` key
parse_qs => true %% Parse query string into `parsed_qs` key
}}
```

Expand Down Expand Up @@ -231,7 +233,7 @@ embeds() ->
| `text` | `TEXT` | binary |
| `boolean` | `BOOLEAN` | boolean |
| `date` | `DATE` | `{Y, M, D}` |
| `utc_datetime` | `TIMESTAMP` | `{{Y,M,D},{H,Mi,S}}` |
| `utc_datetime` | `TIMESTAMPTZ` | `{{Y,M,D},{H,Mi,S}}` |
| `uuid` | `UUID` | binary |
| `jsonb` | `JSONB` | map/list |
| `{enum, [atoms]}` | `VARCHAR(255)` | atom |
Expand Down
6 changes: 3 additions & 3 deletions src/appendix/erlang-essentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ We recommend [mise](https://mise.jdx.dev/) for managing tool versions:
curl https://mise.run | sh

# Install Erlang and rebar3
mise use erlang@26
mise use erlang@27
mise use rebar@3.23

# Verify
Expand All @@ -30,8 +30,8 @@ Alternatively, use [asdf](https://asdf-vm.com/):
```shell
asdf plugin add erlang
asdf plugin add rebar
asdf install erlang 26.2.2
asdf install rebar 3.22.1
asdf install erlang 27.2.2
asdf install rebar 3.23.0
```

## Quick reference
Expand Down
4 changes: 2 additions & 2 deletions src/building-api/json-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ show(#{bindings := #{<<"id">> := Id}}) ->
{status, 404, #{}, #{error => <<"post not found">>}}
end.

create(#{params := Params}) ->
create(#{json := Params}) ->
CS = post:changeset(#{}, Params),
case blog_repo:insert(CS) of
{ok, Post} ->
Expand All @@ -126,7 +126,7 @@ create(#{params := Params}) ->
create(_Req) ->
{status, 422, #{}, #{error => <<"request body required">>}}.

update(#{bindings := #{<<"id">> := Id}, params := Params}) ->
update(#{bindings := #{<<"id">> := Id}, json := Params}) ->
case blog_repo:get(post, binary_to_integer(Id)) of
{ok, Post} ->
CS = post:changeset(Post, Params),
Expand Down
4 changes: 2 additions & 2 deletions src/data-layer/crud.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ show(#{bindings := #{<<"id">> := Id}}) ->
{status, 404, #{}, #{error => <<"post not found">>}}
end.

create(#{params := Params}) ->
create(#{json := Params}) ->
CS = post:changeset(#{}, Params),
case blog_repo:insert(CS) of
{ok, Post} ->
Expand All @@ -160,7 +160,7 @@ create(#{params := Params}) ->
create(_Req) ->
{status, 422, #{}, #{error => <<"request body required">>}}.

update(#{bindings := #{<<"id">> := Id}, params := Params}) ->
update(#{bindings := #{<<"id">> := Id}, json := Params}) ->
case blog_repo:get(post, binary_to_integer(Id)) of
{ok, Post} ->
CS = post:changeset(Post, Params),
Expand Down
2 changes: 1 addition & 1 deletion src/data-layer/schemas-migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Each field has a `name` (atom), `type` (one of Kura's types), and optional prope
| `text` | `TEXT` | binary |
| `boolean` | `BOOLEAN` | boolean |
| `date` | `DATE` | `{Y, M, D}` |
| `utc_datetime` | `TIMESTAMP` | `{{Y,M,D},{H,Mi,S}}` |
| `utc_datetime` | `TIMESTAMPTZ` | `{{Y,M,D},{H,Mi,S}}` |
| `uuid` | `UUID` | binary |
| `jsonb` | `JSONB` | map/list |
| `{enum, [atoms]}` | `VARCHAR(255)` | atom |
Expand Down
9 changes: 7 additions & 2 deletions src/getting-started/views-auth-sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ The security flow for each request is:
3. If `security` is a function, call it with the request map
4. If it returns `{true, AuthData}`, merge `auth_data => AuthData` into the request and continue to the controller
5. If it returns `false`, trigger the 401 error handler
6. If it returns `{redirect, Path}`, redirect without calling the controller
7. If it returns `{false, StatusCode, Headers, Body}`, respond with a custom error

The structured `{false, StatusCode, Headers, Body}` form is useful for APIs where you want to return JSON error details instead of triggering the generic 401 handler.

You can have different security functions for different route groups — one for API token auth, another for session auth, and so on.

Expand Down Expand Up @@ -166,8 +170,9 @@ login_post(#{auth_data := #{authed := true, username := Username}} = Req) ->
{ok, SessionId} = nova_session:generate_session_id(),
Req1 = cowboy_req:set_resp_cookie(<<"session_id">>, SessionId, Req,
#{path => <<"/">>, http_only => true}),
nova_session_ets:set_value(SessionId, <<"username">>, Username),
{redirect, "/"};
Req2 = Req1#{nova_session_id => SessionId},
nova_session:set(Req2, <<"username">>, Username),
{redirect, "/", Req1};
login_post(_Req) ->
{ok, [{error, <<"Invalid username or password">>}], #{view => login}}.

Expand Down
37 changes: 22 additions & 15 deletions src/going-further/plugins-cors.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,11 @@ post_request(Req, _Env, _Options, State) ->
{ok, Req, State}.

plugin_info() ->
{<<"blog_logger_plugin">>,
<<"1.0.0">>,
<<"Blog">>,
<<"Logs request method, path and duration">>,
[]}.
#{title => <<"blog_logger_plugin">>,
version => <<"1.0.0">>,
url => <<"https://github.com/novaframework/nova">>,
authors => [<<"Blog">>],
description => <<"Logs request method, path and duration">>}.
```

Register it as both pre-request and post-request in `sys.config`:
Expand Down Expand Up @@ -169,11 +169,13 @@ post_request(Req, _Env, _Options, State) ->
{ok, Req, State}.

plugin_info() ->
{<<"blog_rate_limit_plugin">>,
<<"1.0.0">>,
<<"Blog">>,
<<"Simple IP-based rate limiting">>,
[max_requests, window_ms]}.
#{title => <<"blog_rate_limit_plugin">>,
version => <<"1.0.0">>,
url => <<"https://github.com/novaframework/nova">>,
authors => [<<"Blog">>],
description => <<"Simple IP-based rate limiting">>,
options => [{max_requests, <<"Max requests per window">>},
{window_ms, <<"Window duration in milliseconds">>}]}.
```

Create the ETS table on application start in `src/blog_app.erl`:
Expand Down Expand Up @@ -290,11 +292,16 @@ post_request(Req, _Env, _Options, State) ->
{ok, Req, State}.

plugin_info() ->
{<<"blog_cors_plugin">>,
<<"1.0.0">>,
<<"Blog">>,
<<"Configurable CORS plugin">>,
[allow_origins, allow_methods, allow_headers, max_age, allow_credentials]}.
#{title => <<"blog_cors_plugin">>,
version => <<"1.0.0">>,
url => <<"https://github.com/novaframework/nova">>,
authors => [<<"Blog">>],
description => <<"Configurable CORS plugin">>,
options => [{allow_origins, <<"Allowed origins">>},
{allow_methods, <<"Allowed HTTP methods">>},
{allow_headers, <<"Allowed headers">>},
{max_age, <<"Preflight cache duration">>},
{allow_credentials, <<"Allow credentials">>}]}.
```

Configure with all options:
Expand Down
2 changes: 1 addition & 1 deletion src/production/websockets.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ my_action(_Req) ->
{xml, <<"<user><name>Alice</name></user>">>}.
```

The handler function receives `(StatusCode, ExtraHeaders, ControllerPayload)` and must return a Cowboy request.
The handler function receives `(ReturnTuple, CallbackFun, Req)` where `ReturnTuple` is the full controller return value. It must return `{ok, Req2}`.

## Fallback controllers

Expand Down
2 changes: 1 addition & 1 deletion src/testing-errors/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ changeset_errors_to_json(#kura_changeset{errors = Errors}) ->
Use it in your controllers:

```erlang
create(#{params := Params}) ->
create(#{json := Params}) ->
CS = post:changeset(#{}, Params),
case blog_repo:insert(CS) of
{ok, Post} ->
Expand Down