fix: route workbench requests to tenant database
This commit is contained in:
+73
-68
@@ -72,7 +72,7 @@ func (a *API) Healthz(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeSuccess(w, r, map[string]any{"status": "ok", "service": "im_workbench"})
|
writeSuccess(w, r, map[string]any{"status": "ok", "service": "im_workbench"})
|
||||||
}
|
}
|
||||||
|
|
||||||
type handlerFunc func(http.ResponseWriter, *http.Request, *auth.Principal)
|
type handlerFunc func(http.ResponseWriter, *http.Request, *auth.Principal, *store.Store)
|
||||||
|
|
||||||
func (a *API) withAuth(next handlerFunc) http.HandlerFunc {
|
func (a *API) withAuth(next handlerFunc) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -86,22 +86,27 @@ func (a *API) withAuth(next handlerFunc) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
next(w, r, principal)
|
st, err := a.store.ForTenant(r.Context(), principal.TenantID, principal.TenantCode)
|
||||||
|
if err != nil {
|
||||||
|
writeStoreError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next(w, r, principal, st)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) workbench(w http.ResponseWriter, r *http.Request, p *auth.Principal) {
|
func (a *API) workbench(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
methodNotAllowed(w, r)
|
methodNotAllowed(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
modules, version, err := a.store.WorkbenchModules(r.Context(), p.UserID)
|
modules, version, err := st.WorkbenchModules(r.Context(), p.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
items := workbenchModulePayloads(modules)
|
items := workbenchModulePayloads(modules)
|
||||||
if err := a.populateUnread(r, p, items); err != nil {
|
if err := a.populateUnread(st, r, p, items); err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -122,7 +127,7 @@ func (a *API) workbench(w http.ResponseWriter, r *http.Request, p *auth.Principa
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) organizationDepartments(w http.ResponseWriter, r *http.Request, p *auth.Principal) {
|
func (a *API) organizationDepartments(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
methodNotAllowed(w, r)
|
methodNotAllowed(w, r)
|
||||||
return
|
return
|
||||||
@@ -131,7 +136,7 @@ func (a *API) organizationDepartments(w http.ResponseWriter, r *http.Request, p
|
|||||||
if parentID == "" {
|
if parentID == "" {
|
||||||
parentID = "root"
|
parentID = "root"
|
||||||
}
|
}
|
||||||
items, next, err := a.store.Departments(r.Context(), p.UserID, parentID, r.URL.Query().Get("cursor"), limit(r, 50, 100))
|
items, next, err := st.Departments(r.Context(), p.UserID, parentID, r.URL.Query().Get("cursor"), limit(r, 50, 100))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -139,7 +144,7 @@ func (a *API) organizationDepartments(w http.ResponseWriter, r *http.Request, p
|
|||||||
writeSuccess(w, r, pagePayload(organizationDepartmentPayloads(items), next))
|
writeSuccess(w, r, pagePayload(organizationDepartmentPayloads(items), next))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) organizationDepartmentResource(w http.ResponseWriter, r *http.Request, p *auth.Principal) {
|
func (a *API) organizationDepartmentResource(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store) {
|
||||||
departmentID, tail, ok := resourceTail(r.URL.Path, "/v1/workbench/organization/departments/")
|
departmentID, tail, ok := resourceTail(r.URL.Path, "/v1/workbench/organization/departments/")
|
||||||
if !ok || tail != "members" {
|
if !ok || tail != "members" {
|
||||||
writeError(w, r, http.StatusNotFound, 30004, reasonMalformedRequest, "organization route not found", nil)
|
writeError(w, r, http.StatusNotFound, 30004, reasonMalformedRequest, "organization route not found", nil)
|
||||||
@@ -149,7 +154,7 @@ func (a *API) organizationDepartmentResource(w http.ResponseWriter, r *http.Requ
|
|||||||
methodNotAllowed(w, r)
|
methodNotAllowed(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
items, next, err := a.store.DepartmentMembers(r.Context(), p.UserID, departmentID, r.URL.Query().Get("cursor"), limit(r, 50, 100))
|
items, next, err := st.DepartmentMembers(r.Context(), p.UserID, departmentID, r.URL.Query().Get("cursor"), limit(r, 50, 100))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -159,13 +164,13 @@ func (a *API) organizationDepartmentResource(w http.ResponseWriter, r *http.Requ
|
|||||||
writeSuccess(w, r, payload)
|
writeSuccess(w, r, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) organizationMembers(w http.ResponseWriter, r *http.Request, p *auth.Principal) {
|
func (a *API) organizationMembers(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
methodNotAllowed(w, r)
|
methodNotAllowed(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
query := firstNonEmpty(r.URL.Query().Get("q"), r.URL.Query().Get("keyword"))
|
query := firstNonEmpty(r.URL.Query().Get("q"), r.URL.Query().Get("keyword"))
|
||||||
items, next, err := a.store.Members(r.Context(), p.UserID, r.URL.Query().Get("department_id"), strings.TrimSpace(query), r.URL.Query().Get("cursor"), limit(r, 50, 100))
|
items, next, err := st.Members(r.Context(), p.UserID, r.URL.Query().Get("department_id"), strings.TrimSpace(query), r.URL.Query().Get("cursor"), limit(r, 50, 100))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -175,7 +180,7 @@ func (a *API) organizationMembers(w http.ResponseWriter, r *http.Request, p *aut
|
|||||||
writeSuccess(w, r, payload)
|
writeSuccess(w, r, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) organizationSearch(w http.ResponseWriter, r *http.Request, p *auth.Principal) {
|
func (a *API) organizationSearch(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
methodNotAllowed(w, r)
|
methodNotAllowed(w, r)
|
||||||
return
|
return
|
||||||
@@ -185,12 +190,12 @@ func (a *API) organizationSearch(w http.ResponseWriter, r *http.Request, p *auth
|
|||||||
writeError(w, r, http.StatusBadRequest, 30000, reasonMalformedRequest, "malformed request", map[string]any{"field": "q"})
|
writeError(w, r, http.StatusBadRequest, 30000, reasonMalformedRequest, "malformed request", map[string]any{"field": "q"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
departments, _, err := a.store.Departments(r.Context(), p.UserID, "root", "", limit(r, 10, 50))
|
departments, _, err := st.Departments(r.Context(), p.UserID, "root", "", limit(r, 10, 50))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
members, _, err := a.store.Members(r.Context(), p.UserID, "", query, "", limit(r, 20, 100))
|
members, _, err := st.Members(r.Context(), p.UserID, "", query, "", limit(r, 20, 100))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -207,7 +212,7 @@ func (a *API) organizationSearch(w http.ResponseWriter, r *http.Request, p *auth
|
|||||||
writeSuccess(w, r, map[string]any{"items": items, "next_cursor": "", "has_more": false})
|
writeSuccess(w, r, map[string]any{"items": items, "next_cursor": "", "has_more": false})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) attendanceConfig(w http.ResponseWriter, r *http.Request, p *auth.Principal) {
|
func (a *API) attendanceConfig(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
methodNotAllowed(w, r)
|
methodNotAllowed(w, r)
|
||||||
return
|
return
|
||||||
@@ -217,7 +222,7 @@ func (a *API) attendanceConfig(w http.ResponseWriter, r *http.Request, p *auth.P
|
|||||||
writeError(w, r, http.StatusBadRequest, 30000, reasonMalformedRequest, "malformed request", map[string]any{"field": "date"})
|
writeError(w, r, http.StatusBadRequest, 30000, reasonMalformedRequest, "malformed request", map[string]any{"field": "date"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cfg, err := a.store.AttendanceConfig(r.Context(), p.UserID, date)
|
cfg, err := st.AttendanceConfig(r.Context(), p.UserID, date)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -225,12 +230,12 @@ func (a *API) attendanceConfig(w http.ResponseWriter, r *http.Request, p *auth.P
|
|||||||
writeSuccess(w, r, attendanceConfigPayload(cfg))
|
writeSuccess(w, r, attendanceConfigPayload(cfg))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) attendanceRecords(w http.ResponseWriter, r *http.Request, p *auth.Principal) {
|
func (a *API) attendanceRecords(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
methodNotAllowed(w, r)
|
methodNotAllowed(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
records, next, stats, err := a.store.AttendanceRecords(r.Context(), p.UserID, store.AttendanceListOptions{
|
records, next, stats, err := st.AttendanceRecords(r.Context(), p.UserID, store.AttendanceListOptions{
|
||||||
Scope: r.URL.Query().Get("scope"),
|
Scope: r.URL.Query().Get("scope"),
|
||||||
From: r.URL.Query().Get("from"),
|
From: r.URL.Query().Get("from"),
|
||||||
To: r.URL.Query().Get("to"),
|
To: r.URL.Query().Get("to"),
|
||||||
@@ -250,7 +255,7 @@ func (a *API) attendanceRecords(w http.ResponseWriter, r *http.Request, p *auth.
|
|||||||
writeSuccess(w, r, payload)
|
writeSuccess(w, r, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) attendanceCheckins(w http.ResponseWriter, r *http.Request, p *auth.Principal) {
|
func (a *API) attendanceCheckins(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
methodNotAllowed(w, r)
|
methodNotAllowed(w, r)
|
||||||
return
|
return
|
||||||
@@ -316,7 +321,7 @@ func (a *API) attendanceCheckins(w http.ResponseWriter, r *http.Request, p *auth
|
|||||||
writeError(w, r, http.StatusBadRequest, 30000, reasonMalformedRequest, "malformed request", map[string]any{"field": "device"})
|
writeError(w, r, http.StatusBadRequest, 30000, reasonMalformedRequest, "malformed request", map[string]any{"field": "device"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
record, checkin, err := a.store.CreateAttendanceCheckin(r.Context(), p.UserID, "attc_"+randomToken(12), store.AttendanceCheckinRequest{
|
record, checkin, err := st.CreateAttendanceCheckin(r.Context(), p.UserID, "attc_"+randomToken(12), store.AttendanceCheckinRequest{
|
||||||
Date: date, Action: action, RuleID: ruleID, ShiftID: shiftID,
|
Date: date, Action: action, RuleID: ruleID, ShiftID: shiftID,
|
||||||
ClientTime: clientTime, Location: location, Wifi: wifi, Device: device, IdempotencyKey: idempotencyKey,
|
ClientTime: clientTime, Location: location, Wifi: wifi, Device: device, IdempotencyKey: idempotencyKey,
|
||||||
})
|
})
|
||||||
@@ -332,10 +337,10 @@ func (a *API) attendanceCheckins(w http.ResponseWriter, r *http.Request, p *auth
|
|||||||
writeSuccess(w, r, map[string]any{"record": attendanceRecordPayload(record), "checkin": attendanceCheckinPayload(*checkin)})
|
writeSuccess(w, r, map[string]any{"record": attendanceRecordPayload(record), "checkin": attendanceCheckinPayload(*checkin)})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) attendanceAppeals(w http.ResponseWriter, r *http.Request, p *auth.Principal) {
|
func (a *API) attendanceAppeals(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store) {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
items, next, err := a.store.AttendanceAppeals(r.Context(), p.UserID, r.URL.Query().Get("status"), r.URL.Query().Get("cursor"), limit(r, 50, 100))
|
items, next, err := st.AttendanceAppeals(r.Context(), p.UserID, r.URL.Query().Get("status"), r.URL.Query().Get("cursor"), limit(r, 50, 100))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -367,7 +372,7 @@ func (a *API) attendanceAppeals(w http.ResponseWriter, r *http.Request, p *auth.
|
|||||||
targetTime = &parsed
|
targetTime = &parsed
|
||||||
}
|
}
|
||||||
attachments, _ := json.Marshal(body.AttachmentFileIDs)
|
attachments, _ := json.Marshal(body.AttachmentFileIDs)
|
||||||
appeal, err := a.store.CreateAttendanceAppeal(r.Context(), p.UserID, store.AttendanceAppeal{
|
appeal, err := st.CreateAttendanceAppeal(r.Context(), p.UserID, store.AttendanceAppeal{
|
||||||
AppealID: "atta_" + randomToken(12), RecordID: strings.TrimSpace(body.RecordID), UserID: p.UserID, AppealType: strings.TrimSpace(body.AppealType),
|
AppealID: "atta_" + randomToken(12), RecordID: strings.TrimSpace(body.RecordID), UserID: p.UserID, AppealType: strings.TrimSpace(body.AppealType),
|
||||||
Reason: strings.TrimSpace(body.Reason), TargetAction: strings.TrimSpace(body.TargetAction), TargetTime: targetTime, AttachmentFileIDs: string(attachments),
|
Reason: strings.TrimSpace(body.Reason), TargetAction: strings.TrimSpace(body.TargetAction), TargetTime: targetTime, AttachmentFileIDs: string(attachments),
|
||||||
}, idempotencyKey)
|
}, idempotencyKey)
|
||||||
@@ -386,10 +391,10 @@ func (a *API) attendanceAppeals(w http.ResponseWriter, r *http.Request, p *auth.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) meetings(w http.ResponseWriter, r *http.Request, p *auth.Principal) {
|
func (a *API) meetings(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store) {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
items, next, err := a.store.Meetings(r.Context(), p.UserID, r.URL.Query().Get("cursor"), limit(r, 50, 100))
|
items, next, err := st.Meetings(r.Context(), p.UserID, r.URL.Query().Get("cursor"), limit(r, 50, 100))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -423,7 +428,7 @@ func (a *API) meetings(w http.ResponseWriter, r *http.Request, p *auth.Principal
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
reminders, _ := json.Marshal(body.ReminderMinutes)
|
reminders, _ := json.Marshal(body.ReminderMinutes)
|
||||||
meeting, err := a.store.CreateMeeting(r.Context(), p.UserID, store.Meeting{
|
meeting, err := st.CreateMeeting(r.Context(), p.UserID, store.Meeting{
|
||||||
MeetingID: "mtg_" + randomToken(12), RecorderID: int64(body.RecorderUserID), Title: strings.TrimSpace(body.Title), StartAt: startAt, EndAt: endAt,
|
MeetingID: "mtg_" + randomToken(12), RecorderID: int64(body.RecorderUserID), Title: strings.TrimSpace(body.Title), StartAt: startAt, EndAt: endAt,
|
||||||
Location: strings.TrimSpace(body.Location), ReminderMinutes: string(reminders), IdempotencyKey: idempotencyKey,
|
Location: strings.TrimSpace(body.Location), ReminderMinutes: string(reminders), IdempotencyKey: idempotencyKey,
|
||||||
}, parseUserIDList(body.AttendeeUserIDs))
|
}, parseUserIDList(body.AttendeeUserIDs))
|
||||||
@@ -443,7 +448,7 @@ func (a *API) meetings(w http.ResponseWriter, r *http.Request, p *auth.Principal
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) meetingResource(w http.ResponseWriter, r *http.Request, p *auth.Principal) {
|
func (a *API) meetingResource(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store) {
|
||||||
meetingID, tail, ok := resourceTail(r.URL.Path, "/v1/workbench/meetings/")
|
meetingID, tail, ok := resourceTail(r.URL.Path, "/v1/workbench/meetings/")
|
||||||
if !ok {
|
if !ok {
|
||||||
writeError(w, r, http.StatusNotFound, 30004, reasonMalformedRequest, "meeting route not found", nil)
|
writeError(w, r, http.StatusNotFound, 30004, reasonMalformedRequest, "meeting route not found", nil)
|
||||||
@@ -451,7 +456,7 @@ func (a *API) meetingResource(w http.ResponseWriter, r *http.Request, p *auth.Pr
|
|||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case tail == "" && r.Method == http.MethodGet:
|
case tail == "" && r.Method == http.MethodGet:
|
||||||
meeting, err := a.store.Meeting(r.Context(), p.UserID, meetingID)
|
meeting, err := st.Meeting(r.Context(), p.UserID, meetingID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -466,7 +471,7 @@ func (a *API) meetingResource(w http.ResponseWriter, r *http.Request, p *auth.Pr
|
|||||||
if !decodeBody(w, r, &body) {
|
if !decodeBody(w, r, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
meeting, err := a.store.UpdateMeeting(r.Context(), p.UserID, meetingID, store.MeetingPatch{Title: body.Title, RecorderID: body.RecorderUserID.ptr(), Version: body.Version})
|
meeting, err := st.UpdateMeeting(r.Context(), p.UserID, meetingID, store.MeetingPatch{Title: body.Title, RecorderID: body.RecorderUserID.ptr(), Version: body.Version})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -478,7 +483,7 @@ func (a *API) meetingResource(w http.ResponseWriter, r *http.Request, p *auth.Pr
|
|||||||
}
|
}
|
||||||
writeSuccess(w, r, meetingPayload(*meeting, p.UserID))
|
writeSuccess(w, r, meetingPayload(*meeting, p.UserID))
|
||||||
case tail == "cancel" && r.Method == http.MethodPost:
|
case tail == "cancel" && r.Method == http.MethodPost:
|
||||||
meeting, err := a.store.CancelMeeting(r.Context(), p.UserID, meetingID)
|
meeting, err := st.CancelMeeting(r.Context(), p.UserID, meetingID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -497,7 +502,7 @@ func (a *API) meetingResource(w http.ResponseWriter, r *http.Request, p *auth.Pr
|
|||||||
if !decodeBody(w, r, &body) {
|
if !decodeBody(w, r, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
meeting, err := a.store.RespondMeeting(r.Context(), p.UserID, meetingID, strings.TrimSpace(body.Response), body.Version)
|
meeting, err := st.RespondMeeting(r.Context(), p.UserID, meetingID, strings.TrimSpace(body.Response), body.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -509,22 +514,22 @@ func (a *API) meetingResource(w http.ResponseWriter, r *http.Request, p *auth.Pr
|
|||||||
}
|
}
|
||||||
writeSuccess(w, r, meetingPayload(*meeting, p.UserID))
|
writeSuccess(w, r, meetingPayload(*meeting, p.UserID))
|
||||||
case tail == "notes" && r.Method == http.MethodGet:
|
case tail == "notes" && r.Method == http.MethodGet:
|
||||||
a.meetingNotesDetail(w, r, p, meetingID)
|
a.meetingNotesDetail(w, r, p, st, meetingID)
|
||||||
case tail == "notes/files" && r.Method == http.MethodPost:
|
case tail == "notes/files" && r.Method == http.MethodPost:
|
||||||
a.meetingNoteFiles(w, r, p, meetingID)
|
a.meetingNoteFiles(w, r, p, st, meetingID)
|
||||||
case strings.HasPrefix(tail, "notes/files/"):
|
case strings.HasPrefix(tail, "notes/files/"):
|
||||||
a.meetingNoteFileResource(w, r, p, meetingID, strings.TrimPrefix(tail, "notes/files/"))
|
a.meetingNoteFileResource(w, r, p, st, meetingID, strings.TrimPrefix(tail, "notes/files/"))
|
||||||
default:
|
default:
|
||||||
methodNotAllowed(w, r)
|
methodNotAllowed(w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) meetingNotes(w http.ResponseWriter, r *http.Request, p *auth.Principal) {
|
func (a *API) meetingNotes(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
methodNotAllowed(w, r)
|
methodNotAllowed(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
items, next, err := a.store.MeetingNotes(r.Context(), p.UserID, r.URL.Query().Get("query"), r.URL.Query().Get("cursor"), limit(r, 50, 100))
|
items, next, err := st.MeetingNotes(r.Context(), p.UserID, r.URL.Query().Get("query"), r.URL.Query().Get("cursor"), limit(r, 50, 100))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -532,13 +537,13 @@ func (a *API) meetingNotes(w http.ResponseWriter, r *http.Request, p *auth.Princ
|
|||||||
writeSuccess(w, r, pagePayload(meetingNoteListPayloads(items, p.UserID), next))
|
writeSuccess(w, r, pagePayload(meetingNoteListPayloads(items, p.UserID), next))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) meetingNotesDetail(w http.ResponseWriter, r *http.Request, p *auth.Principal, meetingID string) {
|
func (a *API) meetingNotesDetail(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store, meetingID string) {
|
||||||
meeting, err := a.store.Meeting(r.Context(), p.UserID, meetingID)
|
meeting, err := st.Meeting(r.Context(), p.UserID, meetingID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
files, err := a.store.MeetingNoteFiles(r.Context(), p.UserID, meetingID)
|
files, err := st.MeetingNoteFiles(r.Context(), p.UserID, meetingID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -546,7 +551,7 @@ func (a *API) meetingNotesDetail(w http.ResponseWriter, r *http.Request, p *auth
|
|||||||
writeSuccess(w, r, meetingNotesDetailPayload(*meeting, files, p.UserID))
|
writeSuccess(w, r, meetingNotesDetailPayload(*meeting, files, p.UserID))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) meetingNoteFiles(w http.ResponseWriter, r *http.Request, p *auth.Principal, meetingID string) {
|
func (a *API) meetingNoteFiles(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store, meetingID string) {
|
||||||
idempotencyKey, ok := requireIdempotencyKey(w, r)
|
idempotencyKey, ok := requireIdempotencyKey(w, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
@@ -557,7 +562,7 @@ func (a *API) meetingNoteFiles(w http.ResponseWriter, r *http.Request, p *auth.P
|
|||||||
if !decodeBody(w, r, &body) {
|
if !decodeBody(w, r, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
file, err := a.store.CreateMeetingNoteFile(r.Context(), p.UserID, meetingID, "mnote_"+randomToken(12), strings.TrimSpace(body.FileID), idempotencyKey)
|
file, err := st.CreateMeetingNoteFile(r.Context(), p.UserID, meetingID, "mnote_"+randomToken(12), strings.TrimSpace(body.FileID), idempotencyKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -570,7 +575,7 @@ func (a *API) meetingNoteFiles(w http.ResponseWriter, r *http.Request, p *auth.P
|
|||||||
writeSuccess(w, r, meetingNoteFilePayload(*file, p.UserID))
|
writeSuccess(w, r, meetingNoteFilePayload(*file, p.UserID))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) meetingNoteFileResource(w http.ResponseWriter, r *http.Request, p *auth.Principal, meetingID, noteFileID string) {
|
func (a *API) meetingNoteFileResource(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store, meetingID, noteFileID string) {
|
||||||
idempotencyKey, ok := requireIdempotencyKey(w, r)
|
idempotencyKey, ok := requireIdempotencyKey(w, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
@@ -584,7 +589,7 @@ func (a *API) meetingNoteFileResource(w http.ResponseWriter, r *http.Request, p
|
|||||||
if !decodeBody(w, r, &body) {
|
if !decodeBody(w, r, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
file, err := a.store.ReplaceMeetingNoteFile(r.Context(), p.UserID, meetingID, noteFileID, strings.TrimSpace(body.FileID), idempotencyKey, body.Version)
|
file, err := st.ReplaceMeetingNoteFile(r.Context(), p.UserID, meetingID, noteFileID, strings.TrimSpace(body.FileID), idempotencyKey, body.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -601,7 +606,7 @@ func (a *API) meetingNoteFileResource(w http.ResponseWriter, r *http.Request, p
|
|||||||
writeError(w, r, http.StatusBadRequest, 30000, reasonMalformedRequest, "malformed request", map[string]any{"field": "version"})
|
writeError(w, r, http.StatusBadRequest, 30000, reasonMalformedRequest, "malformed request", map[string]any{"field": "version"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
file, err := a.store.DeleteMeetingNoteFile(r.Context(), p.UserID, meetingID, noteFileID, idempotencyKey, version)
|
file, err := st.DeleteMeetingNoteFile(r.Context(), p.UserID, meetingID, noteFileID, idempotencyKey, version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -617,10 +622,10 @@ func (a *API) meetingNoteFileResource(w http.ResponseWriter, r *http.Request, p
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) dailyReports(w http.ResponseWriter, r *http.Request, p *auth.Principal) {
|
func (a *API) dailyReports(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store) {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
items, next, err := a.store.DailyReports(r.Context(), p.UserID, r.URL.Query().Get("cursor"), limit(r, 50, 100))
|
items, next, err := st.DailyReports(r.Context(), p.UserID, r.URL.Query().Get("cursor"), limit(r, 50, 100))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -638,7 +643,7 @@ func (a *API) dailyReports(w http.ResponseWriter, r *http.Request, p *auth.Princ
|
|||||||
writeError(w, r, http.StatusBadRequest, 30000, reasonMalformedRequest, "malformed request", map[string]any{"field": "report_date"})
|
writeError(w, r, http.StatusBadRequest, 30000, reasonMalformedRequest, "malformed request", map[string]any{"field": "report_date"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
report, err := a.store.CreateDailyDraft(r.Context(), p.UserID, "dr_"+randomToken(12), date)
|
report, err := st.CreateDailyDraft(r.Context(), p.UserID, "dr_"+randomToken(12), date)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -654,7 +659,7 @@ func (a *API) dailyReports(w http.ResponseWriter, r *http.Request, p *auth.Princ
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) dailyReportResource(w http.ResponseWriter, r *http.Request, p *auth.Principal) {
|
func (a *API) dailyReportResource(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store) {
|
||||||
reportID, tail, ok := resourceTail(r.URL.Path, "/v1/workbench/reports/daily/")
|
reportID, tail, ok := resourceTail(r.URL.Path, "/v1/workbench/reports/daily/")
|
||||||
if !ok {
|
if !ok {
|
||||||
writeError(w, r, http.StatusNotFound, 30004, reasonMalformedRequest, "daily report route not found", nil)
|
writeError(w, r, http.StatusNotFound, 30004, reasonMalformedRequest, "daily report route not found", nil)
|
||||||
@@ -662,7 +667,7 @@ func (a *API) dailyReportResource(w http.ResponseWriter, r *http.Request, p *aut
|
|||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case tail == "" && r.Method == http.MethodGet:
|
case tail == "" && r.Method == http.MethodGet:
|
||||||
report, err := a.store.DailyReport(r.Context(), p.UserID, reportID)
|
report, err := st.DailyReport(r.Context(), p.UserID, reportID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -682,7 +687,7 @@ func (a *API) dailyReportResource(w http.ResponseWriter, r *http.Request, p *aut
|
|||||||
if !decodeBody(w, r, &body) {
|
if !decodeBody(w, r, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
report, err := a.store.SaveDailyDraft(r.Context(), p.UserID, reportID, body.Content, body.Attachments, body.Version)
|
report, err := st.SaveDailyDraft(r.Context(), p.UserID, reportID, body.Content, body.Attachments, body.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -700,7 +705,7 @@ func (a *API) dailyReportResource(w http.ResponseWriter, r *http.Request, p *aut
|
|||||||
if !decodeBody(w, r, &body) {
|
if !decodeBody(w, r, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
report, err := a.store.SubmitDaily(r.Context(), p.UserID, reportID, body.Version)
|
report, err := st.SubmitDaily(r.Context(), p.UserID, reportID, body.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -711,7 +716,7 @@ func (a *API) dailyReportResource(w http.ResponseWriter, r *http.Request, p *aut
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) dailyReportStatistics(w http.ResponseWriter, r *http.Request, p *auth.Principal) {
|
func (a *API) dailyReportStatistics(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
methodNotAllowed(w, r)
|
methodNotAllowed(w, r)
|
||||||
return
|
return
|
||||||
@@ -719,7 +724,7 @@ func (a *API) dailyReportStatistics(w http.ResponseWriter, r *http.Request, p *a
|
|||||||
writeSuccess(w, r, map[string]any{"expected_count": 0, "submitted_count": 0, "missing_count": 0, "scope": map[string]any{"department_id": r.URL.Query().Get("department_id")}})
|
writeSuccess(w, r, map[string]any{"expected_count": 0, "submitted_count": 0, "missing_count": 0, "scope": map[string]any{"department_id": r.URL.Query().Get("department_id")}})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) dailyReportReminders(w http.ResponseWriter, r *http.Request, p *auth.Principal) {
|
func (a *API) dailyReportReminders(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
methodNotAllowed(w, r)
|
methodNotAllowed(w, r)
|
||||||
return
|
return
|
||||||
@@ -743,10 +748,10 @@ func (a *API) dailyReportReminders(w http.ResponseWriter, r *http.Request, p *au
|
|||||||
writeSuccess(w, r, map[string]any{"status": "queued"})
|
writeSuccess(w, r, map[string]any{"status": "queued"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) monthlyReports(w http.ResponseWriter, r *http.Request, p *auth.Principal) {
|
func (a *API) monthlyReports(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store) {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
items, next, err := a.store.MonthlyReports(r.Context(), p.UserID, r.URL.Query().Get("cursor"), limit(r, 50, 100))
|
items, next, err := st.MonthlyReports(r.Context(), p.UserID, r.URL.Query().Get("cursor"), limit(r, 50, 100))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -759,7 +764,7 @@ func (a *API) monthlyReports(w http.ResponseWriter, r *http.Request, p *auth.Pri
|
|||||||
if !decodeBody(w, r, &body) {
|
if !decodeBody(w, r, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
report, err := a.store.CreateMonthlyDraft(r.Context(), p.UserID, "mr_"+randomToken(12), strings.TrimSpace(body.Month))
|
report, err := st.CreateMonthlyDraft(r.Context(), p.UserID, "mr_"+randomToken(12), strings.TrimSpace(body.Month))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -775,7 +780,7 @@ func (a *API) monthlyReports(w http.ResponseWriter, r *http.Request, p *auth.Pri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) monthlyReportResource(w http.ResponseWriter, r *http.Request, p *auth.Principal) {
|
func (a *API) monthlyReportResource(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store) {
|
||||||
reportID, tail, ok := resourceTail(r.URL.Path, "/v1/workbench/reports/monthly/")
|
reportID, tail, ok := resourceTail(r.URL.Path, "/v1/workbench/reports/monthly/")
|
||||||
if !ok {
|
if !ok {
|
||||||
writeError(w, r, http.StatusNotFound, 30004, reasonMalformedRequest, "monthly report route not found", nil)
|
writeError(w, r, http.StatusNotFound, 30004, reasonMalformedRequest, "monthly report route not found", nil)
|
||||||
@@ -783,7 +788,7 @@ func (a *API) monthlyReportResource(w http.ResponseWriter, r *http.Request, p *a
|
|||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case tail == "" && r.Method == http.MethodGet:
|
case tail == "" && r.Method == http.MethodGet:
|
||||||
report, err := a.store.MonthlyReport(r.Context(), p.UserID, reportID)
|
report, err := st.MonthlyReport(r.Context(), p.UserID, reportID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -804,7 +809,7 @@ func (a *API) monthlyReportResource(w http.ResponseWriter, r *http.Request, p *a
|
|||||||
if !decodeBody(w, r, &body) {
|
if !decodeBody(w, r, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
report, err := a.store.SaveMonthlyDraft(r.Context(), p.UserID, reportID, body.WorkSummary, body.NextPlan, body.Attachments, body.Version)
|
report, err := st.SaveMonthlyDraft(r.Context(), p.UserID, reportID, body.WorkSummary, body.NextPlan, body.Attachments, body.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -822,7 +827,7 @@ func (a *API) monthlyReportResource(w http.ResponseWriter, r *http.Request, p *a
|
|||||||
if !decodeBody(w, r, &body) {
|
if !decodeBody(w, r, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
report, err := a.store.SubmitMonthly(r.Context(), p.UserID, reportID, body.Version)
|
report, err := st.SubmitMonthly(r.Context(), p.UserID, reportID, body.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -840,7 +845,7 @@ func (a *API) monthlyReportResource(w http.ResponseWriter, r *http.Request, p *a
|
|||||||
if !decodeBody(w, r, &body) {
|
if !decodeBody(w, r, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
report, err := a.store.ReopenMonthly(r.Context(), p.UserID, reportID, body.Version)
|
report, err := st.ReopenMonthly(r.Context(), p.UserID, reportID, body.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -851,7 +856,7 @@ func (a *API) monthlyReportResource(w http.ResponseWriter, r *http.Request, p *a
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) monthlyReportStatistics(w http.ResponseWriter, r *http.Request, p *auth.Principal) {
|
func (a *API) monthlyReportStatistics(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
methodNotAllowed(w, r)
|
methodNotAllowed(w, r)
|
||||||
return
|
return
|
||||||
@@ -859,12 +864,12 @@ func (a *API) monthlyReportStatistics(w http.ResponseWriter, r *http.Request, p
|
|||||||
writeSuccess(w, r, map[string]any{"expected_count": 0, "submitted_count": 0, "missing_count": 0, "scope": map[string]any{"department_id": r.URL.Query().Get("department_id")}})
|
writeSuccess(w, r, map[string]any{"expected_count": 0, "submitted_count": 0, "missing_count": 0, "scope": map[string]any{"department_id": r.URL.Query().Get("department_id")}})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) mailboxConfig(w http.ResponseWriter, r *http.Request, p *auth.Principal) {
|
func (a *API) mailboxConfig(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
methodNotAllowed(w, r)
|
methodNotAllowed(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cfg, err := a.store.MailboxConfig(r.Context())
|
cfg, err := st.MailboxConfig(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreError(w, r, err)
|
writeStoreError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -872,7 +877,7 @@ func (a *API) mailboxConfig(w http.ResponseWriter, r *http.Request, p *auth.Prin
|
|||||||
writeSuccess(w, r, mailboxConfigPayload(*cfg))
|
writeSuccess(w, r, mailboxConfigPayload(*cfg))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) mailboxSubmissions(w http.ResponseWriter, r *http.Request, p *auth.Principal) {
|
func (a *API) mailboxSubmissions(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
methodNotAllowed(w, r)
|
methodNotAllowed(w, r)
|
||||||
return
|
return
|
||||||
@@ -897,7 +902,7 @@ func (a *API) mailboxSubmissions(w http.ResponseWriter, r *http.Request, p *auth
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
attachments, _ := json.Marshal(body.AttachmentFileIDs)
|
attachments, _ := json.Marshal(body.AttachmentFileIDs)
|
||||||
submission, err := a.store.CreateMailboxSubmission(r.Context(), p.UserID, store.MailboxSubmission{
|
submission, err := st.CreateMailboxSubmission(r.Context(), p.UserID, store.MailboxSubmission{
|
||||||
SubmissionID: "mb_" + randomToken(12), Subject: subject, Content: content,
|
SubmissionID: "mb_" + randomToken(12), Subject: subject, Content: content,
|
||||||
Anonymous: body.Anonymous, AttachmentFileIDs: string(attachments), NoticeVersion: strings.TrimSpace(body.NoticeVersion),
|
Anonymous: body.Anonymous, AttachmentFileIDs: string(attachments), NoticeVersion: strings.TrimSpace(body.NoticeVersion),
|
||||||
}, idempotencyKey)
|
}, idempotencyKey)
|
||||||
@@ -954,10 +959,10 @@ func meetingTargetUsers(meeting store.Meeting) []int64 {
|
|||||||
return targets
|
return targets
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) populateUnread(r *http.Request, p *auth.Principal, modules []map[string]any) error {
|
func (a *API) populateUnread(st *store.Store, r *http.Request, p *auth.Principal, modules []map[string]any) error {
|
||||||
for _, module := range modules {
|
for _, module := range modules {
|
||||||
if module["code"] == "meetings" {
|
if module["code"] == "meetings" {
|
||||||
count, err := a.store.MeetingUnreadCount(r.Context(), p.UserID)
|
count, err := st.MeetingUnreadCount(r.Context(), p.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+121
-4
@@ -7,19 +7,32 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.118im.com/im_team/im_workbench/internal/config"
|
"git.118im.com/im_team/im_workbench/internal/config"
|
||||||
_ "github.com/go-sql-driver/mysql"
|
mysqlDriver "github.com/go-sql-driver/mysql"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Store struct {
|
type Store struct {
|
||||||
db *sqlx.DB
|
db *sqlx.DB
|
||||||
|
dsn string
|
||||||
|
tenantStores map[int64]*Store
|
||||||
|
tenantStoreMu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func Open(cfg config.MySQLConfig) (*Store, error) {
|
func Open(cfg config.MySQLConfig) (*Store, error) {
|
||||||
db, err := sqlx.Open("mysql", cfg.DSN)
|
st, err := openDSN(cfg.DSN)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
st.tenantStores = make(map[int64]*Store)
|
||||||
|
return st, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func openDSN(dsn string) (*Store, error) {
|
||||||
|
db, err := sqlx.Open("mysql", dsn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -32,16 +45,120 @@ func Open(cfg config.MySQLConfig) (*Store, error) {
|
|||||||
_ = db.Close()
|
_ = db.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &Store{db: db}, nil
|
return &Store{db: db, dsn: dsn}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) Close() error {
|
func (s *Store) Close() error {
|
||||||
if s == nil || s.db == nil {
|
if s == nil || s.db == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
s.tenantStoreMu.Lock()
|
||||||
|
for id, st := range s.tenantStores {
|
||||||
|
if st != nil && st != s {
|
||||||
|
_ = st.Close()
|
||||||
|
}
|
||||||
|
delete(s.tenantStores, id)
|
||||||
|
}
|
||||||
|
s.tenantStoreMu.Unlock()
|
||||||
return s.db.Close()
|
return s.db.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type tenantDBConfig struct {
|
||||||
|
ID int64 `db:"id"`
|
||||||
|
Code string `db:"code"`
|
||||||
|
DBName string `db:"db_name"`
|
||||||
|
DBDSN string `db:"db_dsn"`
|
||||||
|
State int `db:"state"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForTenant returns a Store connected to the tenant database configured by manager/hotgo.
|
||||||
|
func (s *Store) ForTenant(ctx context.Context, tenantID int64, tenantCode string) (*Store, error) {
|
||||||
|
if s == nil {
|
||||||
|
return nil, errors.New("store is nil")
|
||||||
|
}
|
||||||
|
if tenantID <= 0 && strings.TrimSpace(tenantCode) == "" {
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
cfg, err := s.tenantDBConfig(ctx, tenantID, tenantCode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if cfg.State != 1 {
|
||||||
|
return nil, fmt.Errorf("tenant disabled: id=%d code=%s", cfg.ID, cfg.Code)
|
||||||
|
}
|
||||||
|
dsn, err := tenantDSN(s.dsn, cfg.DBName, cfg.DBDSN)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if dsn == "" || dsn == s.dsn {
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
cacheID := cfg.ID
|
||||||
|
if cacheID <= 0 {
|
||||||
|
cacheID = tenantID
|
||||||
|
}
|
||||||
|
s.tenantStoreMu.Lock()
|
||||||
|
if st := s.tenantStores[cacheID]; st != nil {
|
||||||
|
s.tenantStoreMu.Unlock()
|
||||||
|
return st, nil
|
||||||
|
}
|
||||||
|
s.tenantStoreMu.Unlock()
|
||||||
|
|
||||||
|
st, err := openDSN(dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.tenantStoreMu.Lock()
|
||||||
|
if existing := s.tenantStores[cacheID]; existing != nil {
|
||||||
|
s.tenantStoreMu.Unlock()
|
||||||
|
_ = st.Close()
|
||||||
|
return existing, nil
|
||||||
|
}
|
||||||
|
s.tenantStores[cacheID] = st
|
||||||
|
s.tenantStoreMu.Unlock()
|
||||||
|
return st, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) tenantDBConfig(ctx context.Context, tenantID int64, tenantCode string) (*tenantDBConfig, error) {
|
||||||
|
var cfg tenantDBConfig
|
||||||
|
if tenantID > 0 {
|
||||||
|
err := s.db.GetContext(ctx, &cfg, `SELECT id,code,IFNULL(db_name,'') AS db_name,IFNULL(db_dsn,'') AS db_dsn,state
|
||||||
|
FROM hotgo.im_tenant WHERE id=? LIMIT 1`, tenantID)
|
||||||
|
if err == nil {
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
|
if !errors.Is(err, sql.ErrNoRows) || strings.TrimSpace(tenantCode) == "" {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err := s.db.GetContext(ctx, &cfg, `SELECT id,code,IFNULL(db_name,'') AS db_name,IFNULL(db_dsn,'') AS db_dsn,state
|
||||||
|
FROM hotgo.im_tenant WHERE code=? LIMIT 1`, strings.TrimSpace(tenantCode))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tenantDSN(baseDSN, dbName, dbDSN string) (string, error) {
|
||||||
|
dbDSN = strings.TrimSpace(dbDSN)
|
||||||
|
if strings.HasPrefix(dbDSN, "mysql:") {
|
||||||
|
dbDSN = strings.TrimPrefix(dbDSN, "mysql:")
|
||||||
|
}
|
||||||
|
if dbDSN != "" {
|
||||||
|
return dbDSN, nil
|
||||||
|
}
|
||||||
|
dbName = strings.TrimSpace(dbName)
|
||||||
|
if dbName == "" {
|
||||||
|
return baseDSN, nil
|
||||||
|
}
|
||||||
|
cfg, err := mysqlDriver.ParseDSN(baseDSN)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
cfg.DBName = dbName
|
||||||
|
return cfg.FormatDSN(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Store) WorkbenchModules(ctx context.Context, userID int64) ([]WorkbenchModule, int, error) {
|
func (s *Store) WorkbenchModules(ctx context.Context, userID int64) ([]WorkbenchModule, int, error) {
|
||||||
var modules []WorkbenchModule
|
var modules []WorkbenchModule
|
||||||
err := s.db.SelectContext(ctx, &modules, `SELECT code,name,IFNULL(icon_file_id,'') AS icon_file_id,route,sort_order,0 AS unread_count,enabled,permission
|
err := s.db.SelectContext(ctx, &modules, `SELECT code,name,IFNULL(icon_file_id,'') AS icon_file_id,route,sort_order,0 AS unread_count,enabled,permission
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTenantDSNUsesConfiguredDSN(t *testing.T) {
|
||||||
|
got, err := tenantDSN("root:pass@tcp(127.0.0.1:3306)/tinode?parseTime=true", "", "mysql:user:secret@tcp(db:3306)/meeting_im?parseTime=true")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tenantDSN returned error: %v", err)
|
||||||
|
}
|
||||||
|
want := "user:secret@tcp(db:3306)/meeting_im?parseTime=true"
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("tenantDSN = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTenantDSNDerivesDBName(t *testing.T) {
|
||||||
|
got, err := tenantDSN("root:pass@tcp(127.0.0.1:3306)/tinode?charset=utf8mb4&parseTime=true&loc=Local", "meeting_im", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tenantDSN returned error: %v", err)
|
||||||
|
}
|
||||||
|
if got == "" || got == "root:pass@tcp(127.0.0.1:3306)/tinode?charset=utf8mb4&parseTime=true&loc=Local" {
|
||||||
|
t.Fatalf("tenantDSN did not derive a tenant database DSN: %q", got)
|
||||||
|
}
|
||||||
|
if parsed := "meeting_im"; !strings.Contains(got, "/"+parsed+"?") {
|
||||||
|
t.Fatalf("tenantDSN = %q, want database %q", got, parsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user