Task Manager
Create, find, update, and delete persistent tasks through an API while applying task lifecycle rules.
Exampletasksmanagementcrudapirestdatabaseexpressionsui
🌅 Horizon
Task Manager at a Glance
Overview
This example models a task manager built around a persistent
TodoItem. Each task has a title, due date, priority, and
lifecycle status. An API supports creating, finding, updating, and
deleting tasks, while a database preserves them.
Architecture
TodoService exposes TodoApi, delegates task
rules to expressions, and reads or writes TodoDB.
User requests enter through the API. The service invokes expressions for task rules and uses the database for persistent CRUD operations.
What It Demonstrates
- Task and lifecycle modeling with
@datatype. -
Persistent CRUD with
@api,@database, and@service. - Default and overdue-status rules with
@expression. -
Task-oriented views with
@dashboardand@ui.
Expected Result
A task-management API and UI backed by persistent storage. Tasks remain
available across requests, support search and updates, start as
inProgress, and can become done or
expired.
🧭 Voyage
1. Voyage Overview
You will build the task manager from its domain model outward. You will define tasks, expose operations, persist data, express business rules, wire the service, add a UI, and package the complete system.
This example uses:
- Gin for the generated REST API;
- PostgreSQL for persistent task storage;
- HTMX, Bootstrap, and Go templates for the generated UI;
- Docker-oriented deployment definitions for the system runtime.
You will follow the dependencies in order, so every new section can use the pieces introduced in the previous ones.
Datatypes provide the shared language. The API, database, expressions, and configuration become a service; dashboards and the API become the UI; deployment brings the complete system together.
2. Model the Datatypes
Define a task
A task manager needs a clear definition of a task, so you will begin
with a datatype named TodoItem. Datatype names start with a
capital letter.
Inside the datatype, you will add fields as name-and-type pairs. For example,
title: String means that every task has a title stored as
text, while dueDate: DateTime gives it a deadline.
The id field is special because it uniquely identifies each
task. Its TI-UUID pattern combines the prefix
TI, for TodoItem, with a generated UUID. The remaining
fields describe the task's priority, lifecycle status, and two values
used later to demonstrate encrypted persistence.
@datatype
TodoItem
id pattern TI-UUID
title: String
dueDate: DateTime
priority: Priority
status: TaskLifecycle
Give priority a fixed set of values
Priority is used as a field type, but still needs a
definition. A priority should only be low, medium, or high. Instead of
allowing any text, you will define those choices with an enum.
enum Priority
low
medium
high
Afterwards, Ocean will be able to use the same three values everywhere—for example, in API inputs, database queries, and UI forms.
Describe the task lifecycle
You will use another enum for TaskLifecycle. A task starts in
progress, may be marked as done, or becomes expired when it passes its
due date. Later, you will add the rules for these changes with
expressions.
enum TaskLifecycle
inProgress
done
expired
Afterwards, the domain model will be ready. The API, database,
expressions, dashboards, and UI will all share the same language:
TodoItem, Priority, and
TaskLifecycle.
3. Define the API
Create a REST API
With the task model defined, users and other applications will need a
way to work with tasks. You will expose a REST API named
TodoApi.
The metadata below tells Ocean how to generate the API:
style selects REST, engine selects Gin,
configType points to the API settings you will define later,
and basePath sets the root URL. The final setting asks Ocean to
generate Swagger documentation.
@api
TodoApi style: rest
engine = gin
configType = ApiConfig
tags = external
version = 1.0.0
description = ToDo API Description
basePath = /
generateSwagger = true
How an endpoint is written
In a REST API, each endpoint follows the same basic shape:
<verb> <path> <method>(<inputs>) : <output>
-
Verb describes the action:
get,post,put, ordelete. -
Path is the URL. Values inside braces, such as
{id}, are path parameters. - Method gives the operation a name that you can connect later in the service.
- Inputs are typed values passed to the method. An underscore means the input is inferred from the path.
- Output is the returned type. An underscore means there is no response body.
Return some service information
You will start with a small endpoint. It takes no input and returns a
string generated by the getInfo method.
get /todo/info getInfo(_) : String
List and retrieve tasks
Listing uses offset and limit path parameters
so callers can page through tasks. Looking up one task uses its ID and
returns a single TodoItem.
get /todo/items/{offset}/{limit} listItems(offset:Int, limit:Int) : List<TodoItem>
get /todo/item/{id} getItem(_) : TodoItem
get /todo/item2/{id} getItem2(_) : TodoItem
getItem and getItem2 intentionally expose the
same kind of lookup. Later, you will connect one through an expression
and the other directly to the database to compare both approaches.
Search for tasks
The next endpoints show three common filters: title only, title and
priority, and title or priority. Notice how Priority from
the datatype page will be reused as an input type.
get /todo/title/{title} findItemByTitle(title:String) : List<TodoItem>
get /todo/title-and-priority/{title}/{priority} findItemByTitleAndPriority(title:String, priority:Priority) : List<TodoItem>
get /todo/title-or-priority/{title}/{priority} findItemByTitleOrPriority(title:String, priority:Priority) : List<TodoItem>
Create a task
Creating data uses post. The method receives a
TodoItem and returns the task after it has been created.
post /todo/item createItem(item:TodoItem) : TodoItem
Update a task
Updating uses put. It accepts the changed task—including a
new lifecycle status—and returns the stored result.
put /todo/item adjustItem(item:TodoItem) : TodoItem
Delete a task
Finally, deletion takes the ID from the URL. Its output is
_ because there is no task to return after deletion.
delete /todo/item/{id} deleteItem(_) : _
Afterwards, the task manager will have its public contract. The next step will be defining where these endpoints preserve their tasks.
4. Define Persistence
Add a database
The API will accept tasks, but those tasks need somewhere to persist.
You will define TodoDB as a PostgreSQL database.
configType tells Ocean which configuration to
use, and encryptionKey = generate enables encrypted fields.
@database
Database TodoDB
engine = postgres
configType = DatabaseConfig
encryptionKey = generate
Turn TodoItem into an entity
A datatype describes the shape of a task; an entity tells the database
to preserve it. You will use key(id) to identify each row
and add indexes for values that will be searched or sorted.
Entity TodoItem
key(id)
indexes: unique(title), index(title), index(dueDate)
Add queries for reading tasks
A query reads data without changing it. Its shape is familiar: a name, typed inputs, and a typed output. These queries will match the list, lookup, and search operations defined in the API.
query listTodoItems(offset:Int, limit:Int) : List<TodoItem>
query findById(id: String) : TodoItem
query findByTitle(title: String) : List<TodoItem>
query findByTitleAndPriority(title: String, priority: Priority) : List<TodoItem>
Add commands for changing tasks
A command changes stored data. Creation and update are transactional,
written as either Transactional or its shorter form
T. Deletion only needs the task ID and returns nothing.
command Transactional createTodoItem(item:TodoItem) : TodoItem
command T updateTodoItem(item:TodoItem) : TodoItem
command deleteTodoItem(id:String) : _
Afterwards, the database will offer everything the API needs. Before wiring them together, you will add the task rules that sit between them.
5. Express the Business Rules
Set the initial status
Expressions are where you will describe business logic. When a task is
created, callers should not decide its starting state. The
CreateItem expression sets it to inProgress,
calls the database command, and returns the checked task.
@expression
CreateItem
input : todo:TodoItem
output : result:TodoItem
logic
todo.status = TaskLifecycle.inProgress
temp = TodoItem.createTodoItem(todo)
result = CheckItem(temp)
Check whether a task is overdue
Small expressions are easy to understand and reuse. This one compares the current time with the task's due date and returns a Boolean.
IsOverdue
input : todo:TodoItem
output : result:Boolean
logic
result = now() > todo.dueDate
Mark unfinished overdue tasks as expired
You will then use IsOverdue inside another expression. If
the task is overdue and not already done, its returned status becomes
expired. Completed tasks stay completed.
CheckItem
input : todo:TodoItem
output : result:TodoItem
logic
overdue = IsOverdue(todo)
if todo.status != TaskLifecycle.done AND overdue then
todo.status = TaskLifecycle.expired
end
result = todo
Use the rule when reading a task
GetItem will first call the database query defined earlier,
then passes the result to CheckItem. This is composition:
each expression will do one small job, and the expressions will combine
into a flow.
GetItem
input : id:String
output : result:TodoItem
logic
temp = TodoItem.findById(id)
result = CheckItem(temp)
You will also find similar expressions for listing, updating, and deleting tasks. Afterwards, both storage operations and business rules will be ready. Configuration will be the next piece needed before the service can be assembled.
6. Configure the Components
Reuse existing configuration types
Configuration will keep environment-specific values out of the model. Ocean already provides PostgreSQL and logging configuration types, so you will import them and give them local names instead of redefining every option.
@config
@import config O.database.postgres.config@1.0.0 as DatabaseConfig
@import config O.log.config@1.0.0 as LogConfig
Collect the backend settings
The backend will need settings for its API, database, and logging. You
will group these under TodoServiceConfig, giving the
service one clear configuration contract.
TodoServiceConfig
apiConfig : ApiConfig
dbConfig : DatabaseConfig
logConfig : LogConfig
Give the API and UI a port
You will then define the small local configuration types. Both the API and UI will need a port, and the UI will also use the imported logging settings. Default values make the example easy to run while remaining overridable.
ApiConfig
port: Int (default=8080)
TodoUiConfig
port: Int (default=8080)
logConfig : LogConfig
Afterwards, all the building blocks needed for the service will be ready.
7. Wire the Service
Assemble the service
A service is where the pieces defined so far will become one working unit. It will use the configuration and database, implement the API on the configured port, and gives each expression a short local alias.
@service
TodoService
use config TodoServiceConfig as myCfg
impl api TodoApi as api on myCfg.apiConfig.port
use database TodoDB as appDb
use expression CreateItem as crt
use expression AdjustItem as adj
use expression GetItem as get
use expression GetAll as all
use expression DeleteItem as dlt
Connect API methods to business logic
A connect statement routes a method on the left to its
implementation on the right. Creation, updates, retrieval, listing, and
deletion go through expressions because they coordinate logic and
persistence.
connect api.createItem -> crt
connect api.adjustItem -> adj
connect api.getItem -> get
connect api.listItems -> all
connect api.deleteItem -> dlt
Connect simple searches directly
Not every method needs an expression. These searches have no extra business rule, so you will connect them straight to the generated entity queries. Ocean lets us choose the simplest useful path for each method.
connect api.findItemByTitle -> TodoItem.findByTitle
connect api.findItemByTitleAndPriority -> TodoItem.findByTitleAndPriority
connect api.findItemByTitleOrPriority -> TodoItem.findByTitleOrPriority
Afterwards, the backend will be complete: requests will enter through the API, follow the connections, apply rules where needed, and reach the database. The next step will give users a visual way to work with it.
8. Build Dashboards and UI
Start with a dashboard
A dashboard describes what users will see and do. For task creation, you will use a single-column layout with a form. The form will ask only for title, priority, and due date—the expression will set the initial status.
@dashboard
TodoSubmitDashboard
title: Create Todo
layout: SingleColumnLayout
Widget CreateTodoForm of type Form
fields:
- title String
- priority Priority
- dueDate DateTime
buttons:
submit : Create Todo
Choose how the UI will be generated
The @ui section will turn dashboards into an application.
Here you will choose HTMX for interactions, Bootstrap for styling, Go HTML
templates for rendering, and Gin for the UI backend.
@ui
TodoUi
framework: htmx
styling: bootstrap
template: go-html-template
backend: go-gin
configType: TodoUiConfig
Add the dashboards to navigation
The example contains dashboards for the main task workflows. You will add them as navigation entries, giving users pages for viewing, searching, creating, editing, and deleting tasks.
nav View dashboard = TodoViewDashboard
nav Search dashboard = TodoSearchDashboard
nav Submit dashboard = TodoSubmitDashboard
nav Edit dashboard = TodoEditDashboard
nav Delete dashboard = TodoDeleteDashboard
Connect user actions to the API
Buttons will not contain business logic. Instead, you will connect each form action to the matching API method. The same contract will then support both programmatic clients and the generated UI.
connect submit.CreateTodoForm.submit -> TodoApi.createItem
connect edit.EditTodoForm.submit -> TodoApi.adjustItem
connect delete.TodoDeleteForm.submit -> TodoApi.deleteItem
Afterwards, the task manager will have both a working backend and a user interface. The last modeling step will describe how all components run together.
9. Define Deployment
Provide a database runtime
The database section describes what needs to be stored. Deployment will
choose how to run it. You will import Ocean's packaged PostgreSQL
service and expose it locally as TodoDatabase.
@deploy
@import service P.db.postgres.docker@1.0.0 as PostgresqlDB
TodoDatabase
service PostgresqlDB
Run the task service
TodoDeploy runs two replicas of TodoService,
exposes its API on port 9091, and uses dependsOn to say
that the database must be available first.
TodoDeploy
service TodoService
replica 2
export 9091:TodoService.api
dependsOn TodoDatabase
Run the user interface
The UI will be a separate deployable component. You will run one replica, expose it on port 8081, and declare that it depends on the task service.
TodoUiDeploy
service TodoUi
replica 1
export 8081:TodoUi.config
dependsOn TodoDeploy
Afterwards, the complete topology will be ready: UI → task service → database. The domain model will remain technology-independent, while Voyage will introduce the choices needed to generate and run it.
10. Validate and Explore
After defining the complete model, you will validate that its pieces fit together and explore what Ocean produces.
- Open the
Task Managerexample in Ocean-lab. - Validate the complete model and resolve any reported references.
- Generate the API, persistence, UI, and deployment artifacts.
- Follow the instructions in the generated
Readme.mdfile to deploy and test the application. - You can access the backend API at
localhost:9091/swagger/index.html. - Access the user interface at
localhost:8081. -
Create a task and confirm its initial status is
inProgress. - Update it to
done, then retrieve it again. - Create an overdue unfinished task and inspect its returned status.
Take a moment to inspect the generated Swagger contract (OpenAPI), database artifacts, dashboards, and deployment output. This is where you can see how the decisions made in one Ocean model flow through every layer.
11. Conclusion
By the end of this example, you will have started with a small
TodoItem and gradually turned it into a complete task
manager. Afterwards, you should understand how to:
- share one typed domain model across multiple DSL sections;
- combine generated persistence with expression-based rules;
- wire API operations directly or through business logic;
- build task-focused dashboards on top of the same API;
- separate the system model from runtime technology configuration.
Useful next experiments include adding task descriptions, introducing a new lifecycle state, filtering by due date, or enforcing allowed status transitions with a finite state machine.
Executable model
<\> Implementation
Explore the runnable model by responsibility, then select a file to inspect its complete source.
# @ocean-meta-start
# tags:
# - task-management
# - documentation
# perspective:
# feature: task-manager
# @ocean-meta-end
@info
name: ToDo App
version: 1.0.0
description: This app helps you to organize your tasks
#
# Component tests for todo-service
#
# Runs against a live instance of the service (see docker-compose.yaml, TodoDeploy
# maps host port 9091 -> container 8080).
#
# Run via: ./bootstrap.sh test
# (invokes `hurl --test --variable host=<SERVICE_TEST_URL_MAP value> test/todo-service/*.hurl`)
# ==============================================================================
# System endpoints
# ==============================================================================
GET {{host}}/info
HTTP 200
[Asserts]
header "Content-Type" contains "application/json"
jsonpath "$.id" == "SVC-TODO-SERVICE"
jsonpath "$.name" == "TodoService"
jsonpath "$.startedAt" isString
GET {{host}}/health
HTTP 200
[Asserts]
jsonpath "$.state" == "healthy"
jsonpath "$.checks" count == 1
jsonpath "$.checks[0].name" == "service"
GET {{host}}/health/live
HTTP 200
[Asserts]
jsonpath "$.state" == "healthy"
jsonpath "$.checks[0].name" == "process"
GET {{host}}/health/ready
HTTP 200
[Asserts]
jsonpath "$.state" == "healthy"
jsonpath "$.checks[0].name" == "readiness"
GET {{host}}/v1/todo/info
HTTP 200
[Asserts]
jsonpath "$" isString
jsonpath "$" contains "Hello from backend!"
# ==============================================================================
# List items (baseline, before creating test data)
# ==============================================================================
GET {{host}}/v1/todo/items/0/100
HTTP 200
[Asserts]
jsonpath "$" isCollection
# Invalid path params on the list endpoint -> 400
GET {{host}}/v1/todo/items/not-a-number/10
HTTP 400
[Asserts]
jsonpath "$.error" == "invalid offset"
GET {{host}}/v1/todo/items/0/not-a-number
HTTP 400
[Asserts]
jsonpath "$.error" == "invalid limit"
# ==============================================================================
# CreateItem — POST /v1/todo/item
# ==============================================================================
POST {{host}}/v1/todo/item
Content-Type: application/json
{
"title": "hurl-component-test-{{newUuid}}",
"dueDate": "2099-12-31T23:59:59Z",
"priority": "medium",
"status": "unknown"
}
HTTP 200
[Captures]
item_id: jsonpath "$.id"
item_title: jsonpath "$.title"
[Asserts]
jsonpath "$.id" startsWith "UR-"
jsonpath "$.priority" == "medium"
# CreateItem always forces status to inProgress, regardless of the input body.
jsonpath "$.status" == "inProgress"
jsonpath "$.dueDate" == "2099-12-31T23:59:59Z"
# Creating an item with a duplicate title violates the unique index on `title`
# and is not classified as a domain error, so it falls back to a generic 500.
POST {{host}}/v1/todo/item
Content-Type: application/json
{
"title": "{{item_title}}",
"dueDate": "2099-12-31T23:59:59Z",
"priority": "low",
"status": "unknown"
}
HTTP 500
[Asserts]
jsonpath "$.error" == "internal server error"
# ==============================================================================
# GetItem — GET /v1/todo/item/:id
# ==============================================================================
GET {{host}}/v1/todo/item/{{item_id}}
HTTP 200
[Asserts]
jsonpath "$.id" == "{{item_id}}"
jsonpath "$.title" == "{{item_title}}"
jsonpath "$.status" == "inProgress"
# GetItem2 — GET /v1/todo/item2/:id (bypasses the overdue/status re-check)
GET {{host}}/v1/todo/item2/{{item_id}}
HTTP 200
[Asserts]
jsonpath "$.id" == "{{item_id}}"
jsonpath "$.title" == "{{item_title}}"
# Unknown id -> DB "record not found" is not classified as a domain error,
# so it also falls back to a generic 500 (not 404).
GET {{host}}/v1/todo/item/does-not-exist
HTTP 500
[Asserts]
jsonpath "$.error" == "internal server error"
GET {{host}}/v1/todo/item2/does-not-exist
HTTP 500
[Asserts]
jsonpath "$.error" == "internal server error"
# ==============================================================================
# FindItemByTitle — GET /v1/todo/title/:title
# ==============================================================================
GET {{host}}/v1/todo/title/{{item_title}}
HTTP 200
[Asserts]
jsonpath "$" count == 1
jsonpath "$[0].id" == "{{item_id}}"
GET {{host}}/v1/todo/title/no-such-title-exists
HTTP 200
[Asserts]
jsonpath "$" count == 0
# ==============================================================================
# FindItemByTitleAndPriority — GET /v1/todo/title-and-priority/:title/:priority
# ==============================================================================
GET {{host}}/v1/todo/title-and-priority/{{item_title}}/medium
HTTP 200
[Asserts]
jsonpath "$" count == 1
jsonpath "$[0].id" == "{{item_id}}"
GET {{host}}/v1/todo/title-and-priority/{{item_title}}/high
HTTP 200
[Asserts]
jsonpath "$" count == 0
# Invalid enum value -> 400
GET {{host}}/v1/todo/title-and-priority/{{item_title}}/urgent
HTTP 400
[Asserts]
jsonpath "$.error" == "invalid priority"
# ==============================================================================
# FindItemByTitleOrPriority — GET /v1/todo/title-or-priority/:title/:priority
# ==============================================================================
GET {{host}}/v1/todo/title-or-priority/{{item_title}}/high
HTTP 200
[Asserts]
# Matches on title even though priority (high) doesn't match.
jsonpath "$" count == 1
jsonpath "$[0].id" == "{{item_id}}"
GET {{host}}/v1/todo/title-or-priority/no-such-title/urgent
HTTP 400
[Asserts]
jsonpath "$.error" == "invalid priority"
# ==============================================================================
# AdjustItem — PUT /v1/todo/item
# ==============================================================================
PUT {{host}}/v1/todo/item
Content-Type: application/json
{
"id": "{{item_id}}",
"title": "{{item_title}}",
"dueDate": "2099-12-31T23:59:59Z",
"priority": "high",
"status": "done"
}
HTTP 200
[Asserts]
jsonpath "$.id" == "{{item_id}}"
jsonpath "$.priority" == "high"
jsonpath "$.status" == "done"
GET {{host}}/v1/todo/item/{{item_id}}
HTTP 200
[Asserts]
jsonpath "$.priority" == "high"
jsonpath "$.status" == "done"
# Updating with a due date in the past flips the status to "expired"
# unless it's already "done" (CheckItem expression).
PUT {{host}}/v1/todo/item
Content-Type: application/json
{
"id": "{{item_id}}",
"title": "{{item_title}}",
"dueDate": "2000-01-01T00:00:00Z",
"priority": "high",
"status": "inProgress"
}
HTTP 200
[Asserts]
jsonpath "$.status" == "expired"
# Adjusting an unknown id -> "record not found" -> generic 500.
PUT {{host}}/v1/todo/item
Content-Type: application/json
{
"id": "does-not-exist",
"title": "irrelevant",
"dueDate": "2099-12-31T23:59:59Z",
"priority": "low",
"status": "unknown"
}
HTTP 500
[Asserts]
jsonpath "$.error" == "internal server error"
# ==============================================================================
# DeleteItem — DELETE /v1/todo/item/:id
# ==============================================================================
DELETE {{host}}/v1/todo/item/{{item_id}}
HTTP 204
GET {{host}}/v1/todo/item/{{item_id}}
HTTP 500
[Asserts]
jsonpath "$.error" == "internal server error"
# Deleting an already-deleted (or never-existing) id is a no-op that still
# reports success: DeleteTodoItem does not check RowsAffected.
DELETE {{host}}/v1/todo/item/{{item_id}}
HTTP 204
# @ocean-meta-start
# tags:
# - task-manager-api
# - rest-api
# perspective:
# feature: task-manager
# service: todo-service
# @ocean-meta-end
@api
TodoApi style: rest
engine = gin
configType = ApiConfig
tags = external
version = 1.0.0
description = ToDo API Description
basePath = /
generateSwagger = true
get /todo/info getInfo(_) : String
get /todo/items/{offset}/{limit} listItems(offset:Int, limit:Int) : List<TodoItem>
get /todo/item/{id} getItem(_) : TodoItem
get /todo/item/ getItem2(id:String in-query) : TodoItem
get /todo/title/{title} findItemByTitle(title:String) : List<TodoItem>
get /todo/title-and-priority/{title}/{priority} findItemByTitleAndPriority(
title : String,
priority : Priority,
) : List<TodoItem>
get /todo/title-or-priority/{title} findItemByTitleOrPriority(
title : String,
priority : Priority in-query,
) : List<TodoItem>
post /todo/item createItem(item:TodoItem) : TodoItem
put /todo/item adjustItem(item:TodoItem) : TodoItem
delete /todo/item/{id} deleteItem(_) : _
# @ocean-meta-start
# tags:
# - configuration
# perspective:
# feature: task-manager
# service: all
# @ocean-meta-end
@config
@import config O.database.postgres.config@1.0.0 as DatabaseConfig
@import config O.log.config@1.0.0 as LogConfig
TodoServiceConfig
apiConfig : ApiConfig
dbConfig : DatabaseConfig
logConfig : LogConfig
TodoUiConfig
port: Int (default=8080)
logConfig : LogConfig
ApiConfig
port: Int (default=8080)
# @ocean-meta-start
# tags:
# - ui
# - dashboard
# perspective:
# feature: task-manager
# service: ui-service
# @ocean-meta-end
@dashboard
TodoViewDashboard
title: Todo Items
subtitle: See all your tasks!
layout: TwoColumnLayout
Widget TodoTable of type Table
title = Tasks Table
columns:
- id String
- title String
- status TaskLifecycle
- dueDate DateTime
TodoSearchDashboard
title: Search Todos
subtitle: Find tasks!
layout: SingleColumnLayout
Widget TodoSearchForm of type Form
title = Find Task
fields:
- title String
- priority Priority
buttons:
submit : Search
cancel : Reset
TodoDeleteDashboard
title: Delete Todos
subtitle: Delete tasks!
layout: SingleColumnLayout
Widget TodoDeleteForm of type Form
title = Delete Task
fields:
- id String
buttons:
submit : Delete
cancel : Reset
TodoSubmitDashboard
title: Create Todo
subtitle: Add tasks!
layout: SingleColumnLayout
Widget CreateTodoForm of type Form
title = Create Task
fields:
- title String
- priority Priority
- dueDate DateTime
buttons:
submit : Create Todo
cancel : Cancel
TodoEditDashboard
title: Edit Todo
subtitle: change tasks!
layout: TwoColumnLayout
Widget EditTodoForm of type Form
title = Edit Task
fields:
- id String
- title String
- dueDate DateTime
- priority Priority
- status TaskLifecycle
buttons:
submit : Update
cancel : Cancel
fetch : 🔄 Fetch by ID
AboutDashboard
title: About
subtitle: App information!
layout: SingleColumnLayout
Widget AboutText of type Text
title = About ToDo App
subtitle = This app helps you to organize your tasks 📝
content = This is the Todo App. Version 1.0.0 ⚡ Built with Ocean-lab.
content = The ToDo App is a simple yet powerful tool designed to help you manage your daily tasks with ease and clarity. Whether you're organizing personal errands, tracking work assignments, or planning long-term goals, this app provides a clean and efficient interface to keep everything under control.<br><br>⚡ Built with Ocean-lab, this application demonstrates the flexibility and expressiveness of our domain-specific design system. It’s lightweight, easy to extend, and serves as a perfect starting point for building more complex productivity tools.
Widget InfoButton of type Button
color = secondary
# @ocean-meta-start
# tags:
# - database
# - postgres
# perspective:
# feature: task-manager
# service: todo-service
# @ocean-meta-end
@database
Database TodoDB
engine = postgres
configType = DatabaseConfig
encryptionKey = generate
tags = primary
Entity TodoItem
key(id)
indexes: unique(title), index(title), index(dueDate)
# Queries
query listTodoItems(offset:Int, limit:Int) : List<TodoItem>
query findById(id: String) : TodoItem
query findByTitle(title: String) : List<TodoItem>
query findByTitleAndPriority(title: String, priority: Priority) : List<TodoItem>
query findByTitleOrPriority(title: String, priority : Priority) : List<TodoItem>
# Commands
command Transactional createTodoItem(item:TodoItem) : TodoItem
command T updateTodoItem(item:TodoItem) : TodoItem
command deleteTodoItem(id:String) : _
# @ocean-meta-start
# tags:
# - deployment
# perspective:
# feature: task-manager
# service: all
# @ocean-meta-end
@deploy
Name: TaskMgt-deploy
@import service P.db.postgres.docker@1.0.0 as PostgresqlDB
TodoDeploy
service TodoService
replica 2
export 9091:TodoService.api
dependsOn TodoDatabase
TodoDatabase
service PostgresqlDB
TodoUiDeploy
service TodoUi
replica 1
export 8081:TodoUi.config
dependsOn TodoDeploy
# @ocean-meta-start
# tags:
# - datatype
# perspective:
# feature: task-manager
# @ocean-meta-end
@datatype
TodoItem
id pattern UR-UUID
title: String
dueDate: DateTime
priority: Priority
status: TaskLifecycle
enum Priority
low
medium
high
enum TaskLifecycle
inProgress
done
expired
# @ocean-meta-start
# tags:
# - expression
# perspective:
# feature: task-manager
# service: backend
# @ocean-meta-end
@expression
GetInfo
input : _
output : res:String
logic
msg = "Hello from backend!"
tgdMsg = TagWrap(msg, "i")
tgdMsg = TagSuffix(tgdMsg, "br")
tip = tipOfTheDay()
tip = StringConcat2("🌟 ", tip)
tgdTip = TagWrap(tip, "b")
tgdTip = TagSuffix(tgdTip, "br")
nh = NowHumanized()
ts = StringConcat2("🕒 ", nh)
tgdTs = TagWrap(ts, "i")
tmp = StringConcat2(tgdMsg, tgdTip)
res = StringConcat2(tmp, tgdTs)
CreateItem
input : todo:TodoItem
output : result:TodoItem
logic
todo.status = TaskLifecycle.inProgress
temp = TodoItem.createTodoItem(todo)
result = CheckItem(temp)
DeleteItem
input : id: String
output : _
logic
TodoItem.deleteTodoItem(id)
AdjustItem
input : todo:TodoItem
output : result:TodoItem
logic
temp = TodoItem.updateTodoItem(todo)
result = CheckItem(temp)
GetItem
input : id:String
output : result:TodoItem
logic
temp = TodoItem.findById(id)
result = CheckItem(temp)
GetAll
input : offset:Int & limit:Int
output : result:List<TodoItem>
logic
temp = TodoItem.listTodoItems(offset, limit)
result = MapSlice(temp, CheckItem)
IsOverdue
input : todo:TodoItem
output : result:Boolean
logic
result = now() > todo.dueDate
CheckItem
input : todo:TodoItem
output : result:TodoItem
logic
overdue = IsOverdue(todo)
if todo.status != TaskLifecycle.done AND overdue then
todo.status = TaskLifecycle.expired
end
result = todo
UnusedExpr
input : todo:TodoItem
output : result:TodoItem
logic
result = todo
# @ocean-meta-start
# tags:
# - task-manager
# - orchestrator
# - service
# perspective:
# feature: task-manager
# service: todo-service
# @ocean-meta-end
@service
TodoService
@perspectives: version:0.1.0, lifestyle:stable
use config TodoServiceConfig as myCfg
impl api TodoApi as api on myCfg.apiConfig.port
use database TodoDB as appDb
use expression GetInfo as info
use expression GetItem as get
use expression GetAll as all
use expression CreateItem as crt
use expression AdjustItem as adj
use expression DeleteItem as dlt
connect myCfg.dbConfig -> appDb
connect api.getInfo -> info
connect api.getItem -> get
connect api.getItem2 -> TodoItem.findById
connect api.findItemByTitle -> TodoItem.findByTitle
connect api.findItemByTitleAndPriority -> TodoItem.findByTitleAndPriority
connect api.findItemByTitleOrPriority -> TodoItem.findByTitleOrPriority
connect api.listItems -> all
connect api.createItem -> crt
connect api.adjustItem -> adj
connect api.deleteItem -> dlt
# @ocean-meta-start
# tags:
# - orchestrator
# - ui
# perspective:
# feature: task-manager
# service: ui-service
# @ocean-meta-end
@ui
TodoUi
@perspectives: version:0.1.0, lifestyle:stable
framework: htmx
styling: bootstrap
template: go-html-template
backend: go-gin
static: static
configType: TodoUiConfig
nav View dashboard = TodoViewDashboard
nav Search dashboard = TodoSearchDashboard
nav Submit dashboard = TodoSubmitDashboard
nav Edit dashboard = TodoEditDashboard
nav Delete dashboard = TodoDeleteDashboard
nav About dashboard = AboutDashboard
nav Company link = https://company.com
nav Schema file = static/schema.pdf
header title = 📝 ToDo App
header subtitle = Simple Task Manager
header align = center
footer title = ⚡ Built with Ocean-lab
footer subtitle = Version 1.0.0
footer align = center
use dashboard TodoViewDashboard as view
use dashboard TodoSearchDashboard as search
use dashboard TodoSubmitDashboard as submit
use dashboard TodoEditDashboard as edit
use dashboard TodoDeleteDashboard as delete
use dashboard AboutDashboard as about
use api TodoApi
connect view.TodoTable.listRows -> TodoApi.listItems
connect search.TodoSearchForm.submit -> TodoApi.findItemByTitleOrPriority # could be AND
connect submit.CreateTodoForm.submit -> TodoApi.createItem
connect edit.EditTodoForm.submit -> TodoApi.adjustItem
connect edit.EditTodoForm.fetch -> TodoApi.getItem
connect delete.TodoDeleteForm.submit -> TodoApi.deleteItem
connect about.InfoButton.click -> TodoApi.getInfo
flowchart
u[User]
subgraph sys[System]
subgraph s[ToDoService]
a[API]
e[Expressions]
end
d[(Database)]
end
u -.communicates.-> a
a --> d
a -.-> e