API SQL Utilisateurs
Une API REST minimale reposant sur SQLite (ou PostgreSQL). Le pool de connexions à la base de données et le serveur HTTP sont tous deux déclarés comme modèles, initialisés une seule fois et partagés entre toutes les tracks de requêtes concurrentes.
Exécution
melodium run 03_sql_user_api/Compo.toml
# ou avec PostgreSQL :
melodium run 03_sql_user_api/Compo.toml --db_url postgres://user:pass@localhost/mydb$ curl http://127.0.0.1:8080/users
OK
$ curl -X POST http://127.0.0.1:8080/users -d '{"name":"Alice","email":"alice@example.com"}'
{"name":"Alice","email":"alice@example.com"}Fonctionnement
Deux modèles sont déclarés en tête de main :
model db: AppDb(db_url=db_url)
model server: HttpServer(host=|from_ipv4(|localhost_ipv4()), port=port)AppDb encapsule SqlPool avec un nombre de connexions min/max configurable. HttpServer accepte toutes les requêtes entrantes et les achemine vers le sous-traitement ayant enregistré la route correspondante.
Séquence de démarrage
Les connexions dans main imposent un ordre de démarrage strict : connect ouvre le pool, connected se déclenche dès que le pool est prêt, déclenchant createTable. Ce n’est qu’après createTable.done que le serveur HTTP démarre, garantissant que le schéma existe avant qu’une requête n’arrive :
treatment main(
const db_url: string = "sqlite://users.db",
const port: u16 = 8080
)
model db: AppDb(db_url=db_url)
model server: HttpServer(host=|from_ipv4(|localhost_ipv4()), port=port)
{
startup()
connect[sql_pool=db]()
startup.trigger -> connect.trigger
connected[sql_pool=db]()
createTable[db=db]()
connected.trigger -> createTable.trigger
start[http_server=server]()
logReady: logInfoMessage(label="server", message="SQL user API ready")
createTable.done -> start.trigger
createTable.done -> logReady.trigger
listUsers[db=db, http_server=server]()
echoCreate[http_server=server]()
}createTable exécute un CREATE TABLE IF NOT EXISTS via executeRaw, puis convertit le nombre de lignes affectées en signal de complétion Block<void> :
treatment createTable[db: SqlPool]()
input trigger: Block<void>
output done: Block<void>
{
executeRaw[sql_pool=db](sql="CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL)")
checkAffected: check<u64>()
emitDone: emit<void>(value=_)
Self.trigger -> executeRaw.trigger,affected -> checkAffected.value,check -> emitDone.trigger,emit -> Self.done
}Gestionnaires de routes
listUsers et echoCreate appellent chacun connection[http_server=server](method=…, route=…) pour enregistrer leur route. Ils s’exécutent de manière concurrente et indépendante ; Mélodium crée automatiquement une nouvelle track pour chaque connexion entrante.
Le pattern bodyTrigger: trigger<byte>() convertit le flux Stream<byte> du corps entrant en un événement Block<void> de démarrage. Le statut, les en-têtes et le traitement du corps sont tous pilotés depuis ce signal unique en parallèle :
treatment listUsers[db: SqlPool, http_server: HttpServer]() {
connection[http_server=http_server](method=|get(), route="/users")
status: emit<HttpStatus>(value=|ok())
headers: emit<StringMap>(value=|smap([]))
bodyTrigger: trigger<byte>()
connection.data -> bodyTrigger.stream,start --> status.trigger,emit -> connection.status
bodyTrigger.start --------> headers.trigger,emit -> connection.headers
queryUsers[db=db]()
bodyTrigger.start -> queryUsers.trigger
logRows: logDataInfos<Map>(label="users")
queryUsers.rows -> logRows.data
okMsg: emit<string>(value="OK\n")
streamMsg: stream<string>()
encode()
bodyTrigger.start -> okMsg.trigger,emit -> streamMsg.block,stream -> encode.text,data -> connection.data
}La --> (double flèche) est le fan-out : une sortie alimente deux entrées simultanément. queryUsers encapsule l’appel sqlFetch, diffusant les lignes du résultat en Stream<Map> :
treatment queryUsers[db: SqlPool]()
input trigger: Block<void>
output rows: Stream<Map>
{
emitBind: emit<Map>(value=|map([]))
sqlFetch[sql_pool=db](sql="SELECT id, name, email FROM users ORDER BY id", bindings=[])
Self.trigger -> emitBind.trigger,emit -> sqlFetch.bind,data -> Self.rows
}echoCreate décode le corps de la requête en JSON et le renvoie tel quel en texte brut :
treatment echoCreate[http_server: HttpServer]() {
connection[http_server=http_server](method=|post(), route="/users")
status: emit<HttpStatus>(value=|ok())
headers: emit<StringMap>(value=|smap([]))
bodyTrigger: trigger<byte>()
connection.data -> bodyTrigger.stream,start --> status.trigger,emit -> connection.status
bodyTrigger.start --------> headers.trigger,emit -> connection.headers
decode()
toJson()
unwrapBody: unwrapOr<Json>(default=|null())
toString<Json>()
encode()
connection.data -> decode.data,text -> toJson.text,json -> unwrapBody.option,value -> toString.value,into -> encode.text,data -> connection.data
}Dépendances
[dependencies]
std = "0.10.1" # flux de base, journalisation, structures de données
http = "0.10.1" # serveur et client HTTP
net = "0.10.1" # utilitaires d'adresses IP
json = "0.10.1" # parsing et sérialisation JSON
sql = "0.10.1" # pool de connexions SQL et requêtes
encoding = "0.10.1" # encodage / décodage UTF-8