mirror of
https://github.com/hanzoai/notify.git
synced 2026-10-30 15:04:32 +00:00
POST /v1/notify/send now defaults to durable async dispatch via
hanzoai/tasks (NotifySendWorkflow, queue notify-send) and returns 202
Accepted with {message_id, task_id, status:"queued"}. ?sync=true keeps
the inline path and returns 200 with the terminal result.
The router fails closed if async is requested but the Dispatcher is
nil or not Started (503) — no silent sync fallback, matching
hanzoai/tasks CONTRACT.md §3 ("production MUST set TASKS_ADDR").
Worker lifecycle moves out of routes/ into cmd/notifyd so the routes
package depends only on the new tasks.Dispatcher interface; tests stub
it directly. Tasks namespace defaults to "default" per CONTRACT.md §6
(single shared worker for all tenants in this PR; per-org namespacing
is the next step once hanzo tenant traffic scales).
A handful of internal helpers narrow from *base.Base to the smaller
core.App interface (Activities, metering.Write/Aggregate, tenant.New,
event.Resolve, template.LoadPublished, mountSend) so tests can drive
them with tests.NewTestApp without booting a full daemon.
Tests:
- internal/tasks/activities_test.go — Deliver idempotency
short-circuits on status=sent and status=failed plus nil-receiver
guard.
- internal/routes/send_test.go — sync 200 / async 202 happy paths +
503-on-missing-dispatcher + 503-on-unstarted-dispatcher +
500-on-dispatch-error + 401-on-missing-X-Org-Id.
References hanzoai/tasks#3.
45 lines
1.2 KiB
Go
45 lines
1.2 KiB
Go
// Package event is the catalog layer over the `events` collection.
|
|
//
|
|
// An Event entry binds (tenant, name) to a default template + routing
|
|
// preferences + a rate limit. The Send pipeline reads the catalog
|
|
// when SendRequest.Event is set, applies the policy, then proceeds.
|
|
//
|
|
// No external state — this package is a thin wrapper over base record
|
|
// operations to keep the routes layer readable.
|
|
package event
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/hanzoai/base/core"
|
|
"github.com/hanzoai/dbx"
|
|
|
|
"github.com/hanzoai/notify/internal/schema"
|
|
)
|
|
|
|
// Resolve loads the enabled event row for (tenant, name) and returns
|
|
// nil (not an error) when no match exists. Empty name short-circuits.
|
|
func Resolve(app core.App, tenant, name string) (*core.Record, error) {
|
|
if tenant == "" {
|
|
return nil, errors.New("event: tenant is required")
|
|
}
|
|
if name == "" {
|
|
return nil, nil
|
|
}
|
|
rows, err := app.FindRecordsByFilter(
|
|
schema.Events,
|
|
"tenant = {:tenant} && name = {:name} && enabled = true",
|
|
"",
|
|
1, 0,
|
|
dbx.Params{"tenant": tenant, "name": name},
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("event: lookup: %w", err)
|
|
}
|
|
if len(rows) == 0 {
|
|
return nil, nil
|
|
}
|
|
return rows[0], nil
|
|
}
|