example: loco-todo-list (#2092)

* example: loco-todo-list

* fmt

* Cargo.lock

* Disabled integration test for GitHub CI

* fmt

* Update Cargo.toml
This commit is contained in:
Billy Chan 2024-02-05 14:35:37 +08:00 committed by GitHub
parent 76f73e778d
commit 7f25da3e2b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 18290 additions and 0 deletions

View File

@ -239,6 +239,7 @@ jobs:
examples/basic, examples/basic,
examples/graphql_example, examples/graphql_example,
examples/jsonrpsee_example, examples/jsonrpsee_example,
examples/loco_example,
examples/poem_example, examples/poem_example,
examples/proxy_gluesql_example, examples/proxy_gluesql_example,
examples/rocket_example, examples/rocket_example,

View File

@ -0,0 +1,3 @@
[alias]
loco = "run --"
playground = "run --example playground"

View File

@ -0,0 +1,5 @@
target
dockerfile
.dockerignore
.git
.gitignore

17
examples/loco_example/.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
**/config/local.yaml
**/config/*.local.yaml
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
!Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

View File

@ -0,0 +1,7 @@
max_width = 100
comment_width = 80
wrap_comments = true
imports_granularity = "Crate"
use_small_heuristics = "Default"
group_imports = "StdExternalCrate"
format_strings = true

4994
examples/loco_example/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,47 @@
[workspace]
[package]
name = "todolist"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
loco-rs = { version = "0.1.7" }
migration = { path = "migration" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
eyre = "0.6"
tokio = { version = "1.33.0", default-features = false }
async-trait = "0.1.74"
tracing = "0.1.40"
chrono = "0.4"
validator = { version = "0.16" }
axum = "0.7.1"
include_dir = "0.7"
uuid = { version = "1.6.0", features = ["v4"] }
tracing-subscriber = { version = "0.3.17", features = ["env-filter", "json"] }
[dependencies.sea-orm]
# path = "../../" # remove this line in your own project
version = "0.12.4" # sea-orm version
features = [
"sqlx-sqlite",
"sqlx-postgres",
"runtime-tokio-rustls",
"macros",
]
[[bin]]
name = "todolist-cli"
path = "src/bin/main.rs"
required-features = []
[dev-dependencies]
serial_test = "2.0.0"
rstest = "0.18.2"
loco-rs = { version = "0.1.7", features = ["testing"] }
insta = { version = "1.34.0", features = ["redactions", "yaml", "filters"] }

View File

@ -0,0 +1,95 @@
![screenshot](Screenshot.png)
> Adapted from https://github.com/loco-rs/todo-list
# Loco with SeaORM example todo list
Build your own todo list website using Loco. Follow the step-by-step guide [here](<(https://loco.rs/blog/frontend-website/)>) to create it effortlessly.
## Build Client
Navigate to the `frontend` directory and build the client:
```sh
$ cd frontend && yarn install && yarn build
vite v5.0.8 building for production...
✓ 120 modules transformed.
dist/index.html 0.46 kB │ gzip: 0.30 kB
dist/assets/index-AbTMZIjW.css 1.26 kB │ gzip: 0.65 kB
dist/assets/index-MJFpQvzE.js 235.64 kB │ gzip: 75.58 kB
✓ built in 2.01s
```
## Run Locally
You need:
* A local postgres instance
Check out your development [configuration](config/development.yaml).
> To configure a database , please run a local postgres database with <code>loco:loco</code> and a db named <code>[app name]_development.</code>:
<code>docker run -d -p 5432:5432 -e POSTGRES_USER=loco -e POSTGRES_DB=[app name]_development -e POSTGRES_PASSWORD="loco" postgres:15.3-alpine</code>
Execute the following command to run your todo list website locally, serving static assets from `frontend/dist`:
```sh
$ cargo loco start
Finished dev [unoptimized + debuginfo] target(s) in 0.53s
Running `target/debug/todolist-cli start`
2024-02-01T08:49:41.070430Z INFO loco_rs::db: auto migrating
2024-02-01T08:49:41.073698Z INFO sea_orm_migration::migrator: Applying all pending migrations
2024-02-01T08:49:41.078191Z INFO sea_orm_migration::migrator: No pending migrations
2024-02-01T08:49:41.100557Z INFO loco_rs::controller::app_routes: [GET] /api/_ping
2024-02-01T08:49:41.100617Z INFO loco_rs::controller::app_routes: [GET] /api/_health
2024-02-01T08:49:41.100667Z INFO loco_rs::controller::app_routes: [GET] /api/notes
2024-02-01T08:49:41.100702Z INFO loco_rs::controller::app_routes: [POST] /api/notes
2024-02-01T08:49:41.100738Z INFO loco_rs::controller::app_routes: [GET] /api/notes/:id
2024-02-01T08:49:41.100791Z INFO loco_rs::controller::app_routes: [DELETE] /api/notes/:id
2024-02-01T08:49:41.100817Z INFO loco_rs::controller::app_routes: [POST] /api/notes/:id
2024-02-01T08:49:41.100934Z INFO loco_rs::controller::app_routes: [Middleware] Adding limit payload data="5mb"
2024-02-01T08:49:41.101017Z INFO loco_rs::controller::app_routes: [Middleware] Adding log trace id
2024-02-01T08:49:41.101057Z INFO loco_rs::controller::app_routes: [Middleware] Adding timeout layer
2024-02-01T08:49:41.101192Z INFO loco_rs::controller::app_routes: [Middleware] Adding cors
2024-02-01T08:49:41.101241Z INFO loco_rs::controller::app_routes: [Middleware] Adding static
▄ ▀
▀ ▄
▄ ▀ ▄ ▄ ▄▀
▄ ▀▄▄
▄ ▀ ▀ ▀▄▀█▄
▀█▄
▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█
██████ █████ ███ █████ ███ █████ ███ ▀█
██████ █████ ███ █████ ▀▀▀ █████ ███ ▄█▄
██████ █████ ███ █████ █████ ███ ████▄
██████ █████ ███ █████ ▄▄▄ █████ ███ █████
██████ █████ ███ ████ ███ █████ ███ ████▀
▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ██▀
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
https://loco.rs
environment: development
database: automigrate
logger: debug
compilation: debug
modes: server
listening on port 3000
```
## Development
To develop the UI, run the following commands:
```sh
$ cd frontend && yarn install && yarn dev
```
To run the server:
```sh
$ cargo loco start
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 KiB

View File

@ -0,0 +1,72 @@
# Loco configuration file documentation
# Application logging configuration
logger:
# Enable or disable logging.
enable: true
# Log level, options: trace, debug, info, warn or error.
level: debug
# Define the logging format. options: compact, pretty or Json
format: compact
# By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries
# Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters.
# override_filter: trace
# Web server configuration
server:
# Port on which the server will listen. the server binding is 0.0.0.0:{PORT}
port: 3000
# The UI hostname or IP address that mailers will point to.
host: http://localhost
# Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block
middlewares:
# Allows to limit the payload size request. payload that bigger than this file will blocked the request.
limit_payload:
# Enable/Disable the middleware.
enable: true
# the limit size. can be b,kb,kib,mb,mib,gb,gib
body_limit: 5mb
# Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details.
logger:
# Enable/Disable the middleware.
enable: true
# when your code is panicked, the request still returns 500 status code.
catch_panic:
# Enable/Disable the middleware.
enable: true
# Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned.
timeout_request:
# Enable/Disable the middleware.
enable: true
# Duration time in milliseconds.
timeout: 5000
cors:
enable: true
static:
enable: true
must_exist: true
folder:
uri: "/"
path: "frontend/dist"
fallback: "frontend/dist/index.html"
# Database Configuration
database:
# Database connection URI
uri: postgres://loco:loco@localhost:5432/loco_app
# When enabled, the sql query will be logged.
enable_logging: false
# Set the timeout duration when acquiring a connection.
connect_timeout: 500
# Set the idle duration before closing a connection.
idle_timeout: 500
# Minimum number of connections for a pool.
min_connections: 1
# Maximum number of connections for a pool.
max_connections: 1
# Run migration up when application loaded
auto_migrate: true
# Truncate database when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode
dangerously_truncate: false
# Recreating schema when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode
dangerously_recreate: false

View File

@ -0,0 +1,72 @@
# Loco configuration file documentation
# Application logging configuration
logger:
# Enable or disable logging.
enable: true
# Log level, options: trace, debug, info, warn or error.
level: debug
# Define the logging format. options: compact, pretty or Json
format: compact
# By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries
# Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters.
# override_filter: trace
# Web server configuration
server:
# Port on which the server will listen. the server binding is 0.0.0.0:{PORT}
port: 3000
# The UI hostname or IP address that mailers will point to.
host: http://localhost
# Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block
middlewares:
# Allows to limit the payload size request. payload that bigger than this file will blocked the request.
limit_payload:
# Enable/Disable the middleware.
enable: true
# the limit size. can be b,kb,kib,mb,mib,gb,gib
body_limit: 5mb
# Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details.
logger:
# Enable/Disable the middleware.
enable: true
# when your code is panicked, the request still returns 500 status code.
catch_panic:
# Enable/Disable the middleware.
enable: true
# Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned.
timeout_request:
# Enable/Disable the middleware.
enable: true
# Duration time in milliseconds.
timeout: 5000
cors:
enable: true
static:
enable: true
must_exist: true
folder:
uri: "/"
path: "frontend/dist"
fallback: "frontend/dist/index.html"
# Database Configuration
database:
# Database connection URI
uri: sqlite://db.sqlite?mode=rwc
# When enabled, the sql query will be logged.
enable_logging: false
# Set the timeout duration when acquiring a connection.
connect_timeout: 500
# Set the idle duration before closing a connection.
idle_timeout: 500
# Minimum number of connections for a pool.
min_connections: 1
# Maximum number of connections for a pool.
max_connections: 1
# Run migration up when application loaded
auto_migrate: true
# Truncate database when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode
dangerously_truncate: false
# Recreating schema when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode
dangerously_recreate: true

View File

@ -0,0 +1,76 @@
# Loco configuration file documentation
# Application logging configuration
logger:
# Enable or disable logging.
enable: false
# Log level, options: trace, debug, info, warn or error.
level: debug
# Define the logging format. options: compact, pretty or Json
format: compact
# By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries
# Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters.
# override_filter: trace
# Web server configuration
server:
# Port on which the server will listen. the server binding is 0.0.0.0:{PORT}
port: 3000
# The UI hostname or IP address that mailers will point to.
host: http://localhost
# Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block
middlewares:
# Allows to limit the payload size request. payload that bigger than this file will blocked the request.
limit_payload:
# Enable/Disable the middleware.
enable: true
# the limit size. can be b,kb,kib,mb,mib,gb,gib
body_limit: 5mb
# Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details.
logger:
# Enable/Disable the middleware.
enable: true
# when your code is panicked, the request still returns 500 status code.
catch_panic:
# Enable/Disable the middleware.
enable: true
# Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned.
timeout_request:
# Enable/Disable the middleware.
enable: true
# Duration time in milliseconds.
timeout: 5000
cors:
enable: true
# Set the value of the [`Access-Control-Allow-Origin`][mdn] header
# allow_origins:
# - https://loco.rs
# Set the value of the [`Access-Control-Allow-Headers`][mdn] header
# allow_headers:
# - Content-Type
# Set the value of the [`Access-Control-Allow-Methods`][mdn] header
# allow_methods:
# - POST
# Set the value of the [`Access-Control-Max-Age`][mdn] header in seconds
# max_age: 3600
# Database Configuration
database:
# Database connection URI
uri: postgres://loco:loco@localhost:5432/loco_app
# When enabled, the sql query will be logged.
enable_logging: false
# Set the timeout duration when acquiring a connection.
connect_timeout: 500
# Set the idle duration before closing a connection.
idle_timeout: 500
# Minimum number of connections for a pool.
min_connections: 1
# Maximum number of connections for a pool.
max_connections: 1
# Run migration up when application loaded
auto_migrate: true
# Truncate database when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode
dangerously_truncate: true
# Recreating schema when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode
dangerously_recreate: false

View File

@ -0,0 +1,18 @@
FROM rust:1.74-slim as builder
WORKDIR /usr/src/
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim
WORKDIR /usr/app
COPY --from=builder /usr/src/frontend/dist /usr/app/frontend/dist
COPY --from=builder /usr/src/frontend/dist/index.html /usr/app/frontend/dist/index.html
COPY --from=builder /usr/src/config /usr/app/config
COPY --from=builder /usr/src/target/release/todolist-cli /usr/app/todolist-cli
ENTRYPOINT ["/usr/app/todolist-cli"]

View File

@ -0,0 +1,20 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

View File

@ -0,0 +1,27 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
.yarn
.yarnrc.yml
.pnp.*
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

View File

@ -0,0 +1,29 @@
{
"name": "frontent",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.6.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-query": "3.39.3",
"react-router-dom": "6.15.0"
},
"devDependencies": {
"@types/react": "18.2.43",
"@types/react-dom": "18.2.17",
"@vitejs/plugin-react": "4.2.1",
"eslint": "8.55.0",
"eslint-plugin-react": "7.33.2",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-react-refresh": "0.4.5",
"vite": "5.0.8"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,37 @@
#root {
max-width: 1280px;
margin: 0 auto;
text-align: center;
}
.logo {
height: 10em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #ff1111aa);
}
.todo-list {
text-align: left;
}
.todo-list .todo{
margin-top: 10px;
}
.todo-add {
margin-top: 20px;
text-align: left;
}
.todo-add input{
width: 70%;
height: 30px;
}
.todo-add button{
float: right;
}

View File

@ -0,0 +1,163 @@
import {
useQuery,
useMutation,
useQueryClient,
} from 'react-query'
import axios from 'axios';
import './App.css'
import { Routes, Route, Outlet, Link } from "react-router-dom";
import { useState } from "react";
export default function App() {
return (
<div>
<h1>Loco Todo List</h1>
<Routes >
<Route path="/" element={<Layout />}>
<Route index element={<TodoList />} />
<Route path="*" element={<NoMatch />} />
</Route>
</Routes>
</div>
)
}
function Layout() {
return (
<div>
<div>
<a href="https://loco.rs" target="_blank" rel="noreferrer">
<img src="https://raw.githubusercontent.com/loco-rs/todo-list-example/4b8ade3ddfb5a2e076e5188cdc8f6cd404f3fdd1/frontend/src/assets/loco.svg" className="logo" alt="Loco logo" />
</a>
</div>
<hr />
<Outlet />
</div>
);
}
function TodoList() {
const queryClient = useQueryClient();
const fetchTodos = async () => {
const { data } = await axios.get(`api/notes`)
return data;
}
const { isLoading, isError, data = [] } = useQuery(["todos"], fetchTodos); // a hook provided by react-query, it takes a key(name) and function that returns a promise
const remove = async (id) => {
try {
const response = await axios.delete(`api/notes/${id}`);
return response.data;
} catch (error) {
console.error('Error posting todo:', error);
throw error;
}
};
const mutation = useMutation(remove, {
onSuccess: () => {
queryClient.invalidateQueries(["todos"]);
},
});
console.log(data)
if (isLoading)
return (
<div className="App">
<p>isLoading...</p>
</div>
);
if (isError)
return (
<div className="App">
<p>Could not get todo list from the server</p>
</div>
);
return (
<div>
<AddTodo />
<div className="todo-list">
{data.map((todo) => (
<div key={todo.id} className="todo" >
<div>
<div> <button onClick={() => {
mutation.mutate(todo.id);
}}>x</button> {todo.title}</div>
</div>
</div>
))}
</div>
</div>
);
}
function AddTodo() {
const [todo, setTodo] = useState("");
const queryClient = useQueryClient();
const add = async (newTodo) => {
try {
const response = await axios.post(`api/notes`, {
title: newTodo,
content: newTodo,
});
return response.data;
} catch (error) {
console.error('Error posting todo:', error);
throw error;
}
};
const mutation = useMutation(add, {
onSuccess: () => {
setTodo("")
queryClient.invalidateQueries(["todos"]);
},
});
return (
<div className='todo-add'>
<input
value={todo}
onChange={(event) => {
setTodo(event.target.value);
}}
type="text"
/>
<button
onClick={() => {
if (todo !== "") {
mutation.mutate(todo);
}
}}
>
Add
</button>
</div>
);
}
function NoMatch() {
return (
<div>
<h2>Sorry, this page not found</h2>
<p>
<Link to="/">Go to the todo list page</Link>
</p>
</div>
);
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@ -0,0 +1,68 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@ -0,0 +1,21 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from "react-router-dom";
import App from './App.jsx'
import './index.css'
import {
QueryClient,
QueryClientProvider,
} from 'react-query'
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<App />
</BrowserRouter>
</QueryClientProvider>
</React.StrictMode>
)

View File

@ -0,0 +1,16 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
proxy: {
"/api": {
target: "http://127.0.0.1:3000",
changeOrigin: true,
secure: false,
},
},
},
})

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
[package]
name = "migration"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
name = "migration"
path = "src/lib.rs"
[dependencies]
async-std = { version = "1", features = ["attributes", "tokio1"] }
loco-rs = { version = "0.1.6" }
[dependencies.sea-orm-migration]
# path = "../../../sea-orm-migration" # remove this line in your own project
version = "0.12.4" # sea-orm-migration version
features = [
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
# e.g.
"runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
]

View File

@ -0,0 +1,41 @@
# Running Migrator CLI
- Generate a new migration file
```sh
cargo run -- migrate generate MIGRATION_NAME
```
- Apply all pending migrations
```sh
cargo run
```
```sh
cargo run -- up
```
- Apply first 10 pending migrations
```sh
cargo run -- up -n 10
```
- Rollback last applied migrations
```sh
cargo run -- down
```
- Rollback last 10 applied migrations
```sh
cargo run -- down -n 10
```
- Drop all tables from the database, then reapply all migrations
```sh
cargo run -- fresh
```
- Rollback all applied migrations, then reapply all migrations
```sh
cargo run -- refresh
```
- Rollback all applied migrations
```sh
cargo run -- reset
```
- Check the status of all migrations
```sh
cargo run -- status
```

View File

@ -0,0 +1,14 @@
#![allow(elided_lifetimes_in_paths)]
#![allow(clippy::wildcard_imports)]
pub use sea_orm_migration::prelude::*;
mod m20231103_114510_notes;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(m20231103_114510_notes::Migration)]
}
}

View File

@ -0,0 +1,36 @@
use std::borrow::BorrowMut;
use loco_rs::schema::*;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
table_auto(Notes::Table)
.col(pk_auto(Notes::Id).borrow_mut())
.col(string_null(Notes::Title).borrow_mut())
.col(string_null(Notes::Content).borrow_mut())
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Notes::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Notes {
Table,
Id,
Title,
Content,
}

View File

@ -0,0 +1,6 @@
use sea_orm_migration::prelude::*;
#[async_std::main]
async fn main() {
cli::run_cli(migration::Migrator).await;
}

View File

@ -0,0 +1,42 @@
use std::path::Path;
use async_trait::async_trait;
use loco_rs::{
app::{AppContext, Hooks},
controller::AppRoutes,
db::{self, truncate_table},
task::Tasks,
worker::Processor,
Result,
};
use sea_orm::DatabaseConnection;
use crate::{controllers, models::_entities::notes};
pub struct App;
#[async_trait]
impl Hooks for App {
fn app_name() -> &'static str {
env!("CARGO_CRATE_NAME")
}
fn routes() -> AppRoutes {
AppRoutes::with_default_routes()
.prefix("/api")
.add_route(controllers::notes::routes())
}
fn connect_workers<'a>(_p: &'a mut Processor, _ctx: &'a AppContext) {}
fn register_tasks(_tasks: &mut Tasks) {}
async fn truncate(db: &DatabaseConnection) -> Result<()> {
truncate_table(db, notes::Entity).await?;
Ok(())
}
async fn seed(db: &DatabaseConnection, base: &Path) -> Result<()> {
db::seed::<notes::ActiveModel>(db, &base.join("notes.yaml").display().to_string()).await?;
Ok(())
}
}

View File

@ -0,0 +1,8 @@
use loco_rs::cli;
use migration::Migrator;
use todolist::app::App;
#[tokio::main]
async fn main() -> eyre::Result<()> {
cli::main::<App, Migrator>().await
}

View File

@ -0,0 +1 @@
pub mod notes;

View File

@ -0,0 +1,69 @@
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::unnecessary_struct_initialization)]
#![allow(clippy::unused_async)]
use loco_rs::prelude::*;
use serde::{Deserialize, Serialize};
use crate::models::_entities::notes::{ActiveModel, Entity, Model};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Params {
pub title: Option<String>,
pub content: Option<String>,
}
impl Params {
fn update(&self, item: &mut ActiveModel) {
item.title = Set(self.title.clone());
item.content = Set(self.content.clone());
}
}
async fn load_item(ctx: &AppContext, id: i32) -> Result<Model> {
let item = Entity::find_by_id(id).one(&ctx.db).await?;
item.ok_or_else(|| Error::NotFound)
}
pub async fn list(State(ctx): State<AppContext>) -> Result<Json<Vec<Model>>> {
format::json(Entity::find().all(&ctx.db).await?)
}
pub async fn add(State(ctx): State<AppContext>, Json(params): Json<Params>) -> Result<Json<Model>> {
let mut item = ActiveModel {
..Default::default()
};
params.update(&mut item);
let item = item.insert(&ctx.db).await?;
format::json(item)
}
pub async fn update(
Path(id): Path<i32>,
State(ctx): State<AppContext>,
Json(params): Json<Params>,
) -> Result<Json<Model>> {
let item = load_item(&ctx, id).await?;
let mut item = item.into_active_model();
params.update(&mut item);
let item = item.update(&ctx.db).await?;
format::json(item)
}
pub async fn remove(Path(id): Path<i32>, State(ctx): State<AppContext>) -> Result<()> {
load_item(&ctx, id).await?.delete(&ctx.db).await?;
format::empty()
}
pub async fn get_one(Path(id): Path<i32>, State(ctx): State<AppContext>) -> Result<Json<Model>> {
format::json(load_item(&ctx, id).await?)
}
pub fn routes() -> Routes {
Routes::new()
.prefix("notes")
.add("/", get(list))
.add("/", post(add))
.add("/:id", get(get_one))
.add("/:id", delete(remove))
.add("/:id", post(update))
}

View File

@ -0,0 +1,11 @@
---
- id: 1
title: Loco note 1
content: Loco note 1 content
created_at: "2023-11-12T12:34:56.789"
updated_at: "2023-11-12T12:34:56.789"
- id: 2
title: Loco note 1
content: Loco note 1 content
created_at: "2023-11-12T12:34:56.789"
updated_at: "2023-11-12T12:34:56.789"

View File

@ -0,0 +1,3 @@
pub mod app;
pub mod controllers;
pub mod models;

View File

@ -0,0 +1,5 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.4
pub mod prelude;
pub mod notes;

View File

@ -0,0 +1,18 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.4
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "notes")]
pub struct Model {
pub created_at: DateTime,
pub updated_at: DateTime,
#[sea_orm(primary_key)]
pub id: i32,
pub title: Option<String>,
pub content: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

View File

@ -0,0 +1,3 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.4
pub use super::notes::Entity as Notes;

View File

@ -0,0 +1,2 @@
pub mod _entities;
pub mod notes;

View File

@ -0,0 +1,7 @@
use sea_orm::entity::prelude::*;
use super::_entities::notes::ActiveModel;
impl ActiveModelBehavior for ActiveModel {
// extend activemodel below (keep comment for generators)
}

View File

@ -0,0 +1,3 @@
mod models;
mod requests;
mod tasks;

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@
mod notes;

View File

@ -0,0 +1,127 @@
use insta::{assert_debug_snapshot, with_settings};
use loco_rs::testing;
use migration::Migrator;
use sea_orm::entity::prelude::*;
use serial_test::serial;
use todolist::{app::App, models::_entities::notes::Entity};
// TODO: see how to dedup / extract this to app-local test utils
// not to framework, because that would require a runtime dep on insta
macro_rules! configure_insta {
($($expr:expr),*) => {
let mut settings = insta::Settings::clone_current();
settings.set_prepend_module_to_snapshot(false);
settings.set_snapshot_suffix("notes_request");
let _guard = settings.bind_to_scope();
};
}
// Disabled integration test for GitHub CI
/*
#[tokio::test]
#[serial]
async fn can_get_notes() {
configure_insta!();
testing::request::<App, Migrator, _, _>(|request, ctx| async move {
testing::seed::<App>(&ctx.db).await.unwrap();
let notes = request.get("/api/notes").await;
with_settings!({
filters => {
let mut combined_filters = testing::CLEANUP_DATE.to_vec();
combined_filters.extend(vec![(r#"\"id\\":\d+"#, r#""id\":ID"#)]);
combined_filters
}
}, {
assert_debug_snapshot!(
(notes.status_code(), notes.text())
);
});
})
.await;
}
#[tokio::test]
#[serial]
async fn can_add_note() {
configure_insta!();
testing::request::<App, Migrator, _, _>(|request, _ctx| async move {
let payload = serde_json::json!({
"title": "loco",
"content": "loco note test",
});
let add_note_request = request.post("/api/notes").json(&payload).await;
with_settings!({
filters => {
let mut combined_filters = testing::CLEANUP_DATE.to_vec();
combined_filters.extend(vec![(r#"\"id\\":\d+"#, r#""id\":ID"#)]);
combined_filters
}
}, {
assert_debug_snapshot!(
(add_note_request.status_code(), add_note_request.text())
);
});
})
.await;
}
#[tokio::test]
#[serial]
async fn can_get_note() {
configure_insta!();
testing::request::<App, Migrator, _, _>(|request, ctx| async move {
testing::seed::<App>(&ctx.db).await.unwrap();
let add_note_request = request.get("/api/notes/1").await;
with_settings!({
filters => {
let mut combined_filters = testing::CLEANUP_DATE.to_vec();
combined_filters.extend(vec![(r#"\"id\\":\d+"#, r#""id\":ID"#)]);
combined_filters
}
}, {
assert_debug_snapshot!(
(add_note_request.status_code(), add_note_request.text())
);
});
})
.await;
}
#[tokio::test]
#[serial]
async fn can_delete_note() {
configure_insta!();
testing::request::<App, Migrator, _, _>(|request, ctx| async move {
testing::seed::<App>(&ctx.db).await.unwrap();
let count_before_delete = Entity::find().all(&ctx.db).await.unwrap().len();
let delete_note_request = request.delete("/api/notes/1").await;
with_settings!({
filters => {
let mut combined_filters = testing::CLEANUP_DATE.to_vec();
combined_filters.extend(vec![(r#"\"id\\":\d+"#, r#""id\":ID"#)]);
combined_filters
}
}, {
assert_debug_snapshot!(
(delete_note_request.status_code(), delete_note_request.text())
);
});
let count_after_delete = Entity::find().all(&ctx.db).await.unwrap().len();
assert_eq!(count_after_delete, count_before_delete - 1);
})
.await;
}
*/

View File

@ -0,0 +1,8 @@
---
source: tests/requests/notes.rs
expression: "(add_note_request.status_code(), add_note_request.text())"
---
(
200,
"{\"created_at\":\"DATE\",\"updated_at\":\"DATE\",\"id\":ID,\"title\":\"loco\",\"content\":\"loco note test\"}",
)

View File

@ -0,0 +1,8 @@
---
source: tests/requests/notes.rs
expression: "(delete_note_request.status_code(), delete_note_request.text())"
---
(
200,
"",
)

View File

@ -0,0 +1,8 @@
---
source: tests/requests/notes.rs
expression: "(add_note_request.status_code(), add_note_request.text())"
---
(
200,
"{\"created_at\":\"DATE\",\"updated_at\":\"DATE\",\"id\":ID,\"title\":\"Loco note 1\",\"content\":\"Loco note 1 content\"}",
)

View File

@ -0,0 +1,8 @@
---
source: tests/requests/notes.rs
expression: "(notes.status_code(), notes.text())"
---
(
200,
"[{\"created_at\":\"DATE\",\"updated_at\":\"DATE\",\"id\":ID,\"title\":\"Loco note 1\",\"content\":\"Loco note 1 content\"},{\"created_at\":\"DATE\",\"updated_at\":\"DATE\",\"id\":ID,\"title\":\"Loco note 1\",\"content\":\"Loco note 1 content\"}]",
)

View File

@ -0,0 +1 @@
pub mod seed;

View File

@ -0,0 +1,42 @@
//! This task implements data seeding functionality for initializing new
//! development/demo environments.
//!
//! # Example
//!
//! Run the task with the following command:
//! ```sh
//! cargo run task
//! ```
//!
//! To override existing data and reset the data structure, use the following
//! command with the `refresh:true` argument:
//! ```sh
//! cargo run task seed_data refresh:true
//! ```
use std::collections::BTreeMap;
use loco_rs::{db, prelude::*};
use migration::Migrator;
use todolist::app::App;
#[allow(clippy::module_name_repetitions)]
pub struct SeedData;
#[async_trait]
impl Task for SeedData {
fn task(&self) -> TaskInfo {
TaskInfo {
name: "seed_data".to_string(),
detail: "Task for seeding data".to_string(),
}
}
async fn run(&self, app_context: &AppContext, vars: &BTreeMap<String, String>) -> Result<()> {
let refresh = vars.get("refresh").is_some_and(|refresh| refresh == "true");
if refresh {
db::reset::<Migrator>(&app_context.db).await?;
}
let path = std::path::Path::new("src/fixtures");
db::run_app_seed::<App>(&app_context.db, path).await?;
Ok(())
}
}