diff --git a/src/appendix/cheat-sheet.md b/src/appendix/cheat-sheet.md index 2e5a6c0..aebb27c 100644 --- a/src/appendix/cheat-sheet.md +++ b/src/appendix/cheat-sheet.md @@ -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 @@ -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 @@ -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 }} ``` @@ -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 | diff --git a/src/appendix/erlang-essentials.md b/src/appendix/erlang-essentials.md index d11fe49..d9c8f27 100644 --- a/src/appendix/erlang-essentials.md +++ b/src/appendix/erlang-essentials.md @@ -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 @@ -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 diff --git a/src/building-api/json-api.md b/src/building-api/json-api.md index 190e1e0..347cf06 100644 --- a/src/building-api/json-api.md +++ b/src/building-api/json-api.md @@ -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} -> @@ -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), diff --git a/src/data-layer/crud.md b/src/data-layer/crud.md index e10308c..6053b89 100644 --- a/src/data-layer/crud.md +++ b/src/data-layer/crud.md @@ -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} -> @@ -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), diff --git a/src/data-layer/schemas-migrations.md b/src/data-layer/schemas-migrations.md index 781d4d0..4644387 100644 --- a/src/data-layer/schemas-migrations.md +++ b/src/data-layer/schemas-migrations.md @@ -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 | diff --git a/src/getting-started/views-auth-sessions.md b/src/getting-started/views-auth-sessions.md index 7d541de..70a81e3 100644 --- a/src/getting-started/views-auth-sessions.md +++ b/src/getting-started/views-auth-sessions.md @@ -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. @@ -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}}. diff --git a/src/going-further/plugins-cors.md b/src/going-further/plugins-cors.md index abb5bb5..41bb4ba 100644 --- a/src/going-further/plugins-cors.md +++ b/src/going-further/plugins-cors.md @@ -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`: @@ -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`: @@ -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: diff --git a/src/production/websockets.md b/src/production/websockets.md index 24f60ef..04cc888 100644 --- a/src/production/websockets.md +++ b/src/production/websockets.md @@ -128,7 +128,7 @@ my_action(_Req) -> {xml, <<"Alice">>}. ``` -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 diff --git a/src/testing-errors/error-handling.md b/src/testing-errors/error-handling.md index a57f4f4..9cd3f95 100644 --- a/src/testing-errors/error-handling.md +++ b/src/testing-errors/error-handling.md @@ -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} ->