Files
zeekay e285e18128 feat: bootstrap notifyd service layer on hanzoai/base + hanzoai/tasks
Replaces the chi-based notifyd scaffold with the canonical
hanzoai/auto-style layout: hanzoai/base as the HTTP + SQLite + auth
substrate (via the platform plugin for IAM JWT validation and
X-Org-Id injection), hanzoai/tasks for durable async sends, and a
per-tenant Notifier resolver that pulls provider credentials from
KMS through platform.KMSClient.

Library at the module root stays untouched — all 33 provider impls
(plivo, twilio, ses, sendgrid, mail, whatsapp, discord, slack, …)
keep their existing public API. The service wraps them.

cmd/notifyd
  - base.New + platform.MustRegister
  - schema.MustRegister (6 collections: tenants, providers,
    templates, events, messages, meter)
  - tasks worker registered when TASKS_ADDR is set; sync path
    runs the same Deliver activity directly when not

internal/
  - boot/      thin platform.KMSClient wiring (env-driven)
  - schema/    6 base collections
  - routes/    /v1/notify/{send,messages,providers,templates,events,metering,health}
               + convenience /send/{sms,email,voice,whatsapp}
  - tenant/    KMS-backed resolver builds library Notifier per send
  - template/  draft → pending → approved → published → archived
  - event/     catalog routing (fills missing channel / template)
  - metering/  per-send ledger writer + aggregate query
  - tasks/     NotifySendWorkflow + Deliver activity (idempotent on
               replay; reloads message row before resending)
  - zaprpc/    procedure name constants (server impl follow-up)

pkg/
  - client/    public HTTP SDK (Send / Message / Providers / Templates)
  - types/     wire shapes shared by routes and client

cucloud: fmt.Errorf(msg) → errors.New(msg) for the go 1.26 vet pass
that landed with this dependency bump. Behaviour unchanged.

Boot smoke-tested locally:
  - SQLite migrations apply cleanly
  - /v1/notify/health returns {"service":"notify","status":"ok"}
  - missing X-Org-Id returns 401 on every authenticated route
  - send with stub tenant returns the expected base validation error
2026-06-01 16:06:27 -07:00

80 lines
2.3 KiB
Go

package routes
import (
"net/http"
"github.com/hanzoai/base"
"github.com/hanzoai/base/apis"
"github.com/hanzoai/base/core"
"github.com/hanzoai/base/tools/router"
"github.com/hanzoai/dbx"
"github.com/hanzoai/notify/internal/schema"
"github.com/hanzoai/notify/pkg/types"
)
// mountMessages installs GET /v1/notify/messages/{id} (single lookup)
// and GET /v1/notify/messages (paginated list).
func mountMessages(r *router.Router[*core.RequestEvent], app *base.Base) {
r.GET("/v1/notify/messages/{id}", func(e *core.RequestEvent) error {
org, err := orgFromRequest(e)
if err != nil {
return err
}
id := e.Request.PathValue("id")
rec, err := app.FindRecordById(schema.Messages, id)
if err != nil || rec == nil {
return apis.NewNotFoundError("message not found", nil)
}
if rec.GetString("tenant") != org {
return apis.NewNotFoundError("message not found", nil) // hide cross-tenant existence
}
return jsonOK(e, toMessageDTO(rec))
})
r.GET("/v1/notify/messages", func(e *core.RequestEvent) error {
org, err := orgFromRequest(e)
if err != nil {
return err
}
records, err := app.FindRecordsByFilter(
schema.Messages,
"tenant = {:tenant}",
"-created", 100, 0,
dbx.Params{"tenant": org},
)
if err != nil {
return apis.NewInternalServerError("list messages", err)
}
out := make([]types.Message, 0, len(records))
for _, r := range records {
out = append(out, toMessageDTO(r))
}
return e.JSON(http.StatusOK, map[string]any{"items": out})
})
}
// toMessageDTO collapses a base record into the public types.Message
// wire shape.
func toMessageDTO(r *core.Record) types.Message {
return types.Message{
ID: r.Id,
TenantSlug: r.GetString("tenant"),
Channel: types.Channel(r.GetString("channel")),
Provider: r.GetString("provider"),
To: r.GetString("to"),
Subject: r.GetString("subject"),
Body: r.GetString("body"),
TemplateID: r.GetString("template_id"),
Event: r.GetString("event"),
Status: r.GetString("status"),
Error: r.GetString("error"),
IdempotencyKey: r.GetString("idempotency_key"),
TaskID: r.GetString("task_id"),
Created: r.GetString("created"),
Updated: r.GetString("updated"),
Sent: r.GetString("sent"),
Delivered: r.GetString("delivered"),
}
}