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
41 changes: 20 additions & 21 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,38 +9,37 @@
- [Create a New Application](getting-started/create-app.md)
- [Routing](getting-started/routing.md)
- [Plugins](getting-started/plugins.md)
- [Views](getting-started/views.md)
- [Authentication](getting-started/authentication.md)
- [Sessions](getting-started/sessions.md)
- [Views, Auth & Sessions](getting-started/views-auth-sessions.md)

# Building APIs
# Data Layer with Kura

- [JSON APIs](building-apis/json-apis.md)
- [WebSockets](building-apis/websockets.md)
- [Database Setup](data-layer/setup.md)
- [Schemas and Migrations](data-layer/schemas-migrations.md)
- [Changesets and Validation](data-layer/changesets.md)
- [CRUD with the Repository](data-layer/crud.md)

# Data and Testing
# Building the API

- [Database Integration](data-and-testing/database-integration.md)
- [Testing](data-and-testing/testing.md)
- [JSON API with Generators](building-api/json-api.md)
- [Associations and Preloading](building-api/associations.md)
- [Tags, Many-to-Many & Embedded Schemas](building-api/advanced-data.md)

# Building a Complete Application
# Testing and Error Handling

- [CRUD Application](building-app/crud-app.md)
- [Error Handling](building-app/error-handling.md)
- [Sub-Applications](building-app/sub-applications.md)
- [Deployment](building-app/deployment.md)
- [Testing](testing-errors/testing.md)
- [Error Handling](testing-errors/error-handling.md)

# Developer Tools
# Real-Time and Production

- [Code Generators](developer-tools/code-generators.md)
- [OpenAPI & API Documentation](developer-tools/openapi.md)
- [Inspection & Audit Tools](developer-tools/inspection-tools.md)
- [WebSockets](production/websockets.md)
- [Pub/Sub and Real-Time Feed](production/pubsub.md)
- [Transactions, Multi & Bulk Operations](production/transactions-bulk.md)
- [Deployment](production/deployment.md)

# Going Further

- [Pub/Sub](going-further/pubsub.md)
- [Custom Plugins](going-further/custom-plugins.md)
- [CORS](going-further/cors.md)
- [OpenAPI, Inspection & Audit](going-further/openapi-tools.md)
- [Custom Plugins and CORS](going-further/plugins-cors.md)
- [OpenTelemetry](going-further/opentelemetry.md)

---
Expand Down
181 changes: 176 additions & 5 deletions src/appendix/cheat-sheet.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Cheat Sheet

Quick reference for Nova's APIs, return values, and configuration.
Quick reference for Nova's APIs, return values, configuration, and Kura's database layer.

## Controller return tuples

Expand Down Expand Up @@ -185,11 +185,175 @@ nova_pubsub:get_local_members(Channel)
]}
```

---

## Kura — Schema definition

```erlang
-module(my_schema).
-behaviour(kura_schema).
-include_lib("kura/include/kura.hrl").
-export([table/0, fields/0, primary_key/0, associations/0, embeds/0]).

table() -> <<"my_table">>.
primary_key() -> id.

fields() ->
[
#kura_field{name = id, type = id, primary_key = true, nullable = false},
#kura_field{name = name, type = string, nullable = false},
#kura_field{name = status, type = {enum, [active, inactive]}},
#kura_field{name = metadata, type = {embed, embeds_one, metadata_schema}},
#kura_field{name = inserted_at, type = utc_datetime},
#kura_field{name = updated_at, type = utc_datetime}
].

associations() ->
[
#kura_assoc{name = author, type = belongs_to, schema = user, foreign_key = author_id},
#kura_assoc{name = comments, type = has_many, schema = comment, foreign_key = post_id},
#kura_assoc{name = tags, type = many_to_many, schema = tag,
join_through = <<"posts_tags">>, join_keys = {post_id, tag_id}}
].

embeds() ->
[#kura_embed{name = metadata, type = embeds_one, schema = metadata_schema}].
```

### Kura field types

| Type | PostgreSQL | Erlang |
|---|---|---|
| `id` | `BIGSERIAL` | integer |
| `integer` | `INTEGER` | integer |
| `float` | `DOUBLE PRECISION` | float |
| `string` | `VARCHAR(255)` | binary |
| `text` | `TEXT` | binary |
| `boolean` | `BOOLEAN` | boolean |
| `date` | `DATE` | `{Y, M, D}` |
| `utc_datetime` | `TIMESTAMP` | `{{Y,M,D},{H,Mi,S}}` |
| `uuid` | `UUID` | binary |
| `jsonb` | `JSONB` | map/list |
| `{enum, [atoms]}` | `VARCHAR(255)` | atom |
| `{array, Type}` | `Type[]` | list |
| `{embed, embeds_one, Mod}` | `JSONB` | map |
| `{embed, embeds_many, Mod}` | `JSONB` | list of maps |

## Kura — Changeset API

```erlang
%% Create a changeset
CS = kura_changeset:cast(SchemaModule, ExistingData, Params, AllowedFields).

%% Validations
kura_changeset:validate_required(CS, [field1, field2])
kura_changeset:validate_format(CS, field, "regex")
kura_changeset:validate_length(CS, field, [{min, 3}, {max, 200}])
kura_changeset:validate_number(CS, field, [{greater_than, 0}])
kura_changeset:validate_inclusion(CS, field, [val1, val2, val3])
kura_changeset:validate_change(CS, field, fun(Val) -> ok | {error, Msg} end)

%% Constraint declarations
kura_changeset:unique_constraint(CS, field)
kura_changeset:foreign_key_constraint(CS, field)
kura_changeset:check_constraint(CS, ConstraintName, field, #{message => Msg})

%% Association/embed casting
kura_changeset:cast_assoc(CS, assoc_name)
kura_changeset:cast_assoc(CS, assoc_name, #{with => Fun})
kura_changeset:put_assoc(CS, assoc_name, Value)
kura_changeset:cast_embed(CS, embed_name)

%% Changeset helpers
kura_changeset:get_change(CS, field) -> Value | undefined
kura_changeset:get_field(CS, field) -> Value | undefined
kura_changeset:put_change(CS, field, Val) -> CS1
kura_changeset:add_error(CS, field, Msg) -> CS1
kura_changeset:apply_changes(CS) -> DataMap
kura_changeset:apply_action(CS, Action) -> {ok, Data} | {error, CS}
```

### Schemaless changesets

```erlang
Types = #{email => string, age => integer},
CS = kura_changeset:cast(Types, #{}, Params, [email, age]).
```

## Kura — Query builder

```erlang
Q = kura_query:from(schema_module),

%% Where conditions
Q1 = kura_query:where(Q, {field, value}), %% =
Q1 = kura_query:where(Q, {field, '>', value}), %% comparison
Q1 = kura_query:where(Q, {field, in, [val1, val2]}), %% IN
Q1 = kura_query:where(Q, {field, ilike, <<"%term%">>}), %% ILIKE
Q1 = kura_query:where(Q, {field, is_nil}), %% IS NULL
Q1 = kura_query:where(Q, {'or', [{f1, v1}, {f2, v2}]}), %% OR

%% Ordering, pagination
Q2 = kura_query:order_by(Q, [{field, asc}]),
Q3 = kura_query:limit(Q, 10),
Q4 = kura_query:offset(Q, 20),

%% Preloading associations
Q5 = kura_query:preload(Q, [author, {comments, [author]}]).
```

## Kura — Repository API

```erlang
%% Read
blog_repo:all(Query) -> {ok, [Map]}
blog_repo:get(Schema, Id) -> {ok, Map} | {error, not_found}
blog_repo:get_by(Schema, Clauses) -> {ok, Map} | {error, not_found}
blog_repo:one(Query) -> {ok, Map} | {error, not_found}

%% Write
blog_repo:insert(Changeset) -> {ok, Map} | {error, Changeset}
blog_repo:insert(Changeset, Opts) -> {ok, Map} | {error, Changeset}
blog_repo:update(Changeset) -> {ok, Map} | {error, Changeset}
blog_repo:delete(Changeset) -> {ok, Map} | {error, Changeset}

%% Bulk
blog_repo:insert_all(Schema, [Map]) -> {ok, Count}
blog_repo:update_all(Query, Updates) -> {ok, Count}
blog_repo:delete_all(Query) -> {ok, Count}

%% Preloading
blog_repo:preload(Schema, Records, Assocs) -> Records

%% Transactions
blog_repo:transaction(Fun) -> {ok, Result} | {error, Reason}
blog_repo:multi(Multi) -> {ok, Results} | {error, Step, Value, Completed}
```

### Upsert options

```erlang
blog_repo:insert(CS, #{on_conflict => {field, nothing}})
blog_repo:insert(CS, #{on_conflict => {field, replace_all}})
blog_repo:insert(CS, #{on_conflict => {field, {replace, [fields]}}})
```

## Kura — Multi (transaction pipelines)

```erlang
M = kura_multi:new(),
M1 = kura_multi:insert(M, step_name, Changeset),
M2 = kura_multi:update(M1, step_name, fun(Results) -> Changeset end),
M3 = kura_multi:delete(M2, step_name, Changeset),
M4 = kura_multi:run(M3, step_name, fun(Results) -> {ok, Value} end),
{ok, #{step1 := V1, step2 := V2}} = blog_repo:multi(M4).
```

## Common rebar3 commands

| Command | Description |
|---|---|
| `rebar3 compile` | Compile the project |
| `rebar3 compile` | Compile the project (also triggers kura migration generation) |
| `rebar3 shell` | Start interactive shell |
| `rebar3 nova serve` | Dev server with hot-reload |
| `rebar3 nova routes` | List registered routes |
Expand All @@ -200,7 +364,7 @@ nova_pubsub:get_local_members(Channel)
| `rebar3 as prod tar` | Build release tarball |
| `rebar3 dialyzer` | Run type checker |

## Nova developer tool commands
## rebar3_nova commands

| Command | Description |
|---|---|
Expand All @@ -213,6 +377,13 @@ nova_pubsub:get_local_members(Channel)
| `rebar3 nova audit` | Find routes missing security callbacks |
| `rebar3 nova release` | Build release with auto-generated OpenAPI |

## rebar3_kura commands

| Command | Description |
|---|---|
| `rebar3 kura setup --name REPO` | Generate a repo module and migrations directory |
| `rebar3 kura compile` | Diff schemas vs migrations and generate new migrations |

### Generator options

```shell
Expand All @@ -222,6 +393,6 @@ rebar3 nova gen_controller --name products --actions list,show,create
# OpenAPI with custom output
rebar3 nova openapi --output priv/assets/openapi.json --title "My API" --api-version 1.0.0

# Release with specific profile
rebar3 nova release --profile staging
# Kura setup with custom repo name
rebar3 kura setup --name my_repo
```
8 changes: 4 additions & 4 deletions src/appendix/erlang-essentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ handle(#{method := <<"GET">>} = Req) -> get_handler(Req);
handle(#{method := <<"POST">>} = Req) -> post_handler(Req).

%% Case expression
case pgo:query("SELECT ...") of
#{rows := [Row]} -> {ok, Row};
#{rows := []} -> {error, not_found}
case blog_repo:get(post, Id) of
{ok, Post} -> handle_post(Post);
{error, not_found} -> not_found
end.
```

Expand Down Expand Up @@ -140,7 +140,7 @@ An OTP application is a component with a defined start/stop lifecycle. Your Nova

### Supervisors

Supervisors manage child processes and restart them if they crash. The generated `my_first_nova_sup.erl` is your application's supervisor.
Supervisors manage child processes and restart them if they crash. The generated `blog_sup.erl` is your application's supervisor.

### gen_server

Expand Down
Loading