// Package todos: todo lists and items with assignment. package todos import ( "context" "errors" "fmt" "atlas9.dev/c/core" "atlas9.dev/c/core/iam" "atlas9.dev/c/demo/lib/filter" "atlas9.dev/c/demo/lib/slack" ) // List groups items. An item can belong to any number of lists // within its tenant. type List struct { ID core.ID Tenant core.ID Name string } type Item struct { ID core.ID Tenant core.ID Title string Notes string Status Status // Assignee is the user assigned to the item. Empty means unassigned. // Assignees must be members of the item's tenant. Assignee core.ID } // ListEntry is a row of the item↔list join: one item's presence in one list. // It is the read side of AddToList / RemoveFromList — list the entries for a // set of items and group by Item. type ListEntry struct { Tenant core.ID List core.ID Item core.ID } //atlas:enum Status Open InProgress Done var ( Cap_Todos_CreateList = iam.NewCap("Todos_CreateList") Cap_Todos_UpdateList = iam.NewCap("Todos_UpdateList") Cap_Todos_ReadList = iam.NewCap("Todos_ReadList") Cap_Todos_DeleteList = iam.NewCap("Todos_DeleteList") Cap_Todos_AddToList = iam.NewCap("Todos_AddToList") Cap_Todos_RemoveFromList = iam.NewCap("Todos_RemoveFromList") Cap_Todos_CreateItem = iam.NewCap("Todos_CreateItem") Cap_Todos_UpdateItem = iam.NewCap("Todos_UpdateItem") Cap_Todos_ReadItem = iam.NewCap("Todos_ReadItem") Cap_Todos_DeleteItem = iam.NewCap("Todos_DeleteItem") ) var ErrAssigneeNotMember = errors.New("assignee is not a member of the tenant") // Webhook event types emitted for item changes. Subscribers register these // strings on an endpoint's EventTypes. const ( EventItemCreated = "Todos_ItemCreated" EventItemUpdated = "Todos_ItemUpdated" ) // ItemCreated and ItemUpdated are the domain events emitted when a todo item // changes. Each carries its own audit entry and channel renderings as methods; // the dispatcher (demo/events) asserts those methods to route the event, so the // per-event mapping lives here with the feature that owns it. type ItemCreated struct{ Item Item } func (ItemCreated) EventType() string { return EventItemCreated } func (e ItemCreated) Audit() iam.AuditEntry { return iam.AuditEntry{ Action: "Todos_CreateItem", Resource: e.Item.ID.String(), Detail: e.Item.Title, } } func (e ItemCreated) WebhookPayload() any { return e.Item } func (e ItemCreated) SlackMessage() slack.Message { return slack.Message{ Text: fmt.Sprintf("📝 New todo created: %q", e.Item.Title), } } type ItemUpdated struct{ Item Item } func (ItemUpdated) EventType() string { return EventItemUpdated } func (e ItemUpdated) Audit() iam.AuditEntry { return iam.AuditEntry{ Action: "Todos_UpdateItem", Resource: e.Item.ID.String(), Detail: e.Item.Title, } } func (e ItemUpdated) WebhookPayload() any { return e.Item } func (e ItemUpdated) SlackMessage() slack.Message { return slack.Message{ Text: fmt.Sprintf("✏️ Todo updated: %q — %s", e.Item.Title, e.Item.Status), } } // ItemSchema declares the filterable fields for todo items. var ItemSchema = filter.Schema{}. Enum("status", StatusUnknown). String("assignee"). String("assignee_id"). String("title"). String("list"). String("list_id"). Default("title") type Store interface { CreateList(ctx context.Context, list *List) error UpdateList(ctx context.Context, list *List) error GetList(ctx context.Context, tenant core.ID, id core.ID, out *List) error ListLists(ctx context.Context, tenant core.ID, page core.PageReq, out *core.Page[List]) error DeleteList(ctx context.Context, tenant core.ID, id core.ID) error AddToList(ctx context.Context, tenant core.ID, list core.ID, item core.ID) error RemoveFromList(ctx context.Context, tenant core.ID, list core.ID, item core.ID) error // ListEntriesByItems lists item↔list entries for a set of items, so a client // with a page of items resolves all their lists in one call. The result is // bounded by the caller's item set, so it is not itself paginated. ListEntriesByItems(ctx context.Context, tenant core.ID, items []core.ID, out *core.Page[ListEntry]) error CreateItem(ctx context.Context, item *Item) error UpdateItem(ctx context.Context, item *Item) error GetItem(ctx context.Context, tenant core.ID, id core.ID, out *Item) error ListItems(ctx context.Context, tenant core.ID, expr filter.Expr, page core.PageReq, out *core.Page[Item]) error DeleteItem(ctx context.Context, tenant core.ID, id core.ID) error }