fx build fix
Some checks failed
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/fx/1 Pipeline failed
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/fx/2 Pipeline failed

This commit is contained in:
Stephan D
2025-11-08 00:30:29 +01:00
parent 590fad0071
commit 49b86efecb
165 changed files with 9466 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
package papitemplate
import (
"context"
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
"github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.uber.org/zap"
)
func (a *ProtectedAPI[T]) archive(r *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
objectRef, err := a.Cph.GetRef(r)
if err != nil {
a.Logger.Warn("Failed to restore object reference", zap.Error(err), mutil.PLog(a.Cph, r))
return response.BadReference(a.Logger, a.Name(), a.Cph.Name(), a.Cph.GetID(r), err)
}
organizationRef, err := a.Oph.GetRef(r)
if err != nil {
a.Logger.Warn("Failed to restore organization reference", zap.Error(err), mutil.PLog(a.Oph, r))
return response.BadReference(a.Logger, a.Name(), a.Oph.Name(), a.Oph.GetID(r), err)
}
archived, err := mutil.GetArchiveParam(a.Logger, r)
if err != nil {
a.Logger.Warn("Failed to read optional 'archived' param", zap.Error(err))
return response.Auto(a.Logger, a.resource, err)
}
if archived == nil {
a.Logger.Warn("No archivation setting provided")
return response.BadRequest(a.Logger, a.resource, "invalid_query_parameter", "'archived' pram must be present")
}
cascade, err := mutil.GetCascadeParam(a.Logger, r)
if err != nil {
a.Logger.Warn("Failed to read optional 'cascade' param", zap.Error(err))
return response.Auto(a.Logger, a.resource, err)
}
if cascade == nil {
a.Logger.Warn("Cascade property not specified, defaulting to false")
csc := false
cascade = &csc
}
ctx := r.Context()
_, err = a.a.DBFactory().TransactionFactory().CreateTransaction().Execute(ctx, func(ctx context.Context) (any, error) {
return nil, a.DB.SetArchived(r.Context(), *account.GetID(), organizationRef, objectRef, *archived, *cascade)
})
if err != nil {
a.Logger.Warn("Failed to change archive property", zap.Error(err), mzap.StorableRef(account), mutil.PLog(a.Cph, r),
zap.Bool("archived", *archived), zap.Bool("cascade", *cascade))
return response.Auto(a.Logger, a.Name(), err)
}
if a.nconfig.NeedArchiveNotification {
var object T
if err := a.DB.Get(ctx, *account.GetID(), objectRef, &object); err != nil {
a.Logger.Warn("Failed to fetch object for notification", zap.Error(err), mzap.StorableRef(account), mutil.PLog(a.Cph, r))
} else {
if err := a.nconfig.ArchiveNotification(&object, *account.GetID()); err != nil {
a.Logger.Warn("Failed to send archivation notification", zap.Error(err), mzap.StorableRef(account), mutil.PLog(a.Cph, r))
}
}
}
return a.Objects([]T{}, accessToken)
}

View File

@@ -0,0 +1,133 @@
package papitemplate
import (
"github.com/tech/sendico/server/interface/api/sresponse"
)
type HandlerResolver func(sresponse.AccountHandlerFunc) sresponse.AccountHandlerFunc
type Config interface {
WithNoCreate() Config
WithCreateHandler(handler sresponse.AccountHandlerFunc) Config
WithNoList() Config
WithListHandler(handler sresponse.AccountHandlerFunc) Config
WithNoGet() Config
WithGetHandler(handler sresponse.AccountHandlerFunc) Config
WithNoUpdate() Config
WithUpdateHandler(handler sresponse.AccountHandlerFunc) Config
WithNoDelete() Config
WithDeleteHandler(handler sresponse.AccountHandlerFunc) Config
WithReorderHandler(reorder ReorderConfig) Config
WithTaggableHandler(taggable TaggableConfig) Config
}
type PAPIConfig struct {
CreateResolver HandlerResolver
ListResolver HandlerResolver
GetResolver HandlerResolver
UpdateResolver HandlerResolver
DeleteResolver HandlerResolver
ArchiveResolver HandlerResolver
Reorder *ReorderConfig
Taggable *TaggableConfig
}
// WithNoCreate disables the create endpoint by replacing its resolver.
func (cfg *PAPIConfig) WithNoCreate() *PAPIConfig {
cfg.CreateResolver = disableResolver
return cfg
}
// WithCreateHandler overrides the create endpoint by replacing its resolver.
func (cfg *PAPIConfig) WithCreateHandler(handler sresponse.AccountHandlerFunc) *PAPIConfig {
cfg.CreateResolver = overrideResolver(handler)
return cfg
}
// WithNoList disables the list endpoint.
func (cfg *PAPIConfig) WithNoList() *PAPIConfig {
cfg.ListResolver = disableResolver
return cfg
}
// WithListHandler overrides the list endpoint.
func (cfg *PAPIConfig) WithListHandler(handler sresponse.AccountHandlerFunc) *PAPIConfig {
cfg.ListResolver = overrideResolver(handler)
return cfg
}
// WithNoGet disables the get endpoint.
func (cfg *PAPIConfig) WithNoGet() *PAPIConfig {
cfg.GetResolver = disableResolver
return cfg
}
// WithGetHandler overrides the get endpoint.
func (cfg *PAPIConfig) WithGetHandler(handler sresponse.AccountHandlerFunc) *PAPIConfig {
cfg.GetResolver = overrideResolver(handler)
return cfg
}
// WithNoUpdate disables the update endpoint.
func (cfg *PAPIConfig) WithNoUpdate() *PAPIConfig {
cfg.UpdateResolver = disableResolver
return cfg
}
// WithUpdateHandler overrides the update endpoint.
func (cfg *PAPIConfig) WithUpdateHandler(handler sresponse.AccountHandlerFunc) *PAPIConfig {
cfg.UpdateResolver = overrideResolver(handler)
return cfg
}
// WithNoDelete disables the delete endpoint.
func (cfg *PAPIConfig) WithNoDelete() *PAPIConfig {
cfg.DeleteResolver = disableResolver
return cfg
}
// WithDeleteHandler overrides the delete endpoint.
func (cfg *PAPIConfig) WithDeleteHandler(handler sresponse.AccountHandlerFunc) *PAPIConfig {
cfg.DeleteResolver = overrideResolver(handler)
return cfg
}
func (cfg *PAPIConfig) WithNoArchive() *PAPIConfig {
cfg.ArchiveResolver = disableResolver
return cfg
}
func (cfg *PAPIConfig) WithArchiveHandler(handler sresponse.AccountHandlerFunc) *PAPIConfig {
cfg.ArchiveResolver = overrideResolver(handler)
return cfg
}
// defaultResolver returns the default handler unchanged.
func defaultResolver(defaultHandler sresponse.AccountHandlerFunc) sresponse.AccountHandlerFunc {
return defaultHandler
}
// disableResolver always returns nil, disabling the endpoint.
func disableResolver(_ sresponse.AccountHandlerFunc) sresponse.AccountHandlerFunc {
return nil
}
// overrideResolver returns a resolver that always returns the given custom handler.
func overrideResolver(custom sresponse.AccountHandlerFunc) HandlerResolver {
return func(_ sresponse.AccountHandlerFunc) sresponse.AccountHandlerFunc {
return custom
}
}
func NewConfig() *PAPIConfig {
return &PAPIConfig{
CreateResolver: defaultResolver,
ListResolver: defaultResolver,
GetResolver: defaultResolver,
UpdateResolver: defaultResolver,
DeleteResolver: defaultResolver,
ArchiveResolver: defaultResolver,
Reorder: nil,
Taggable: nil,
}
}

View File

@@ -0,0 +1,38 @@
package papitemplate
import (
"encoding/json"
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
"github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.uber.org/zap"
)
func (a *ProtectedAPI[T]) create(r *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
organizationRef, err := a.Oph.GetRef(r)
if err != nil {
a.Logger.Warn("Failed to parse parent object reference", zap.Error(err), mutil.PLog(a.Oph, r))
return response.BadReference(a.Logger, a.Name(), a.Oph.Name(), a.Oph.GetID(r), err)
}
var object T
if err := json.NewDecoder(r.Body).Decode(&object); err != nil {
a.Logger.Warn("Failed to decode object when creating", zap.Error(err), mzap.StorableRef(account), mutil.PLog(a.Oph, r))
return response.BadPayload(a.Logger, a.Name(), err)
}
if err := a.DB.Create(r.Context(), *account.GetID(), organizationRef, &object); err != nil {
a.Logger.Warn("Error creating object", zap.Error(err), mzap.StorableRef(account), mutil.PLog(a.Oph, r), mutil.PLog(a.Cph, r))
return response.Auto(a.Logger, a.Name(), err)
}
if err := a.nconfig.CreateNotification(&object, *account.GetID()); err != nil {
a.Logger.Warn("Failed to send creation notification", zap.Error(err), mzap.StorableRef(account), mutil.PLog(a.Oph, r), mutil.PLog(a.Cph, r))
}
return a.ObjectCreated(&object, accessToken)
}

View File

@@ -0,0 +1,23 @@
package papitemplate
import (
"context"
"github.com/tech/sendico/pkg/db/repository/builder"
"github.com/tech/sendico/pkg/model"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type ProtectedDB[T any] interface {
Create(ctx context.Context, accountRef, organizationRef primitive.ObjectID, object *T) error
Get(ctx context.Context, accountRef, objectRef primitive.ObjectID, result *T) error
Update(ctx context.Context, accountRef primitive.ObjectID, object *T) error
Delete(ctx context.Context, accountRef, objectRef primitive.ObjectID) error
DeleteCascadeAuth(ctx context.Context, accountRef, objectRef primitive.ObjectID) error
SetArchived(ctx context.Context, accountRef, organizationRef, objectRef primitive.ObjectID, isArchived, cascade bool) error
List(ctx context.Context, accountRef, organizationRef, parentRef primitive.ObjectID, cursor *model.ViewCursor) ([]T, error)
}
type ReorderDB interface {
Reorder(ctx context.Context, accountRef, objectRef primitive.ObjectID, newIndex int, filter builder.Query) error
}

View File

@@ -0,0 +1,67 @@
package papitemplate
import (
"context"
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
"github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
func (a *ProtectedAPI[T]) deleteImp(ctx context.Context, account *model.Account, objectRef primitive.ObjectID, cascade *bool) error {
var err error
if (cascade != nil) && (*cascade) {
_, err = a.a.DBFactory().TransactionFactory().CreateTransaction().Execute(ctx, func(ctx context.Context) (any, error) {
return nil, a.DB.DeleteCascadeAuth(ctx, *account.GetID(), objectRef)
})
} else {
err = a.DB.Delete(ctx, *account.GetID(), objectRef)
}
if err != nil {
a.Logger.Warn("Error deleting object", zap.Error(err), mzap.StorableRef(account), mzap.ObjRef("object_ref", objectRef))
return err
}
return nil
}
func (a *ProtectedAPI[T]) delete(r *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
objectRef, err := a.Cph.GetRef(r)
if err != nil {
a.Logger.Warn("Failed to restore object reference", zap.Error(err), mutil.PLog(a.Cph, r))
return response.BadReference(a.Logger, a.Name(), a.Cph.Name(), a.Cph.GetID(r), err)
}
cascade, err := mutil.GetCascadeParam(a.Logger, r)
if err != nil {
a.Logger.Warn("Failed to read optional 'cascade' param", zap.Error(err))
return response.Auto(a.Logger, a.resource, err)
}
var objPtr *T
if a.nconfig.NeedDeleteNotification {
var object T
if err := a.DB.Get(r.Context(), *account.GetID(), objectRef, &object); err != nil {
a.Logger.Warn("Failed to fetch object for notification", zap.Error(err), mzap.StorableRef(account), mutil.PLog(a.Cph, r))
} else {
objPtr = &object
}
}
if err := a.deleteImp(r.Context(), account, objectRef, cascade); err != nil {
a.Logger.Warn("Error deleting object", zap.Error(err), mzap.StorableRef(account), mutil.PLog(a.Cph, r))
return response.Auto(a.Logger, a.Name(), err)
}
if objPtr != nil {
if err := a.nconfig.DeleteNotification(objPtr, *account.GetID()); err != nil {
a.Logger.Warn("Failed to send deletion notification", zap.Error(err), mzap.StorableRef(account), mutil.PLog(a.Cph, r))
}
}
return a.Objects([]T{}, accessToken)
}

View File

@@ -0,0 +1,29 @@
package papitemplate
import (
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
"github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.uber.org/zap"
)
func (a *ProtectedAPI[T]) get(r *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
ctx := r.Context()
objectRef, err := a.Cph.GetRef(r)
if err != nil {
a.Logger.Warn("Failed to restore object reference", zap.Error(err), mutil.PLog(a.Cph, r))
return response.BadReference(a.Logger, a.Name(), a.Cph.Name(), a.Cph.GetID(r), err)
}
var object T
if err := a.DB.Get(ctx, *account.GetID(), objectRef, &object); err != nil {
a.Logger.Warn("Failed to fetch object", zap.Error(err), mzap.StorableRef(account), mutil.PLog(a.Cph, r))
return response.Auto(a.Logger, a.Name(), err)
}
return a.Object(&object, accessToken)
}

View File

@@ -0,0 +1,42 @@
package papitemplate
import (
"errors"
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.uber.org/zap"
)
func (a *ProtectedAPI[T]) list(r *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
organizationRef, err := a.Oph.GetRef(r)
if err != nil {
a.Logger.Warn("Failed to restore organization reference", zap.Error(err), mutil.PLog(a.Oph, r))
return response.BadReference(a.Logger, a.Name(), a.Oph.Name(), a.Oph.GetID(r), err)
}
parentRef, err := a.Pph.GetRef(r)
if err != nil {
a.Logger.Warn("Failed to restore parent reference", zap.Error(err), mutil.PLog(a.Pph, r))
return response.BadReference(a.Logger, a.Name(), a.Pph.Name(), a.Pph.GetID(r), err)
}
cursor, err := mutil.GetViewCursor(a.Logger, r)
if err != nil {
a.Logger.Warn("Failed to decode view cursor", zap.Error(err))
return response.Auto(a.Logger, a.Name(), err)
}
objects, err := a.DB.List(r.Context(), *account.GetID(), organizationRef, parentRef, cursor)
if err != nil {
if !errors.Is(err, merrors.ErrNoData) {
a.Logger.Warn("Failed to list objects", zap.Error(err), mutil.PLog(a.Pph, r))
return response.Auto(a.Logger, a.Name(), err)
} else {
a.Logger.Debug("No objects available", zap.Error(err), mutil.PLog(a.Pph, r))
}
}
return a.Objects(objects, accessToken)
}

View File

@@ -0,0 +1,88 @@
package papitemplate
import (
"github.com/tech/sendico/pkg/messaging"
notifications "github.com/tech/sendico/pkg/messaging/envelope"
model "github.com/tech/sendico/pkg/model/notification"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// NotificationHandler is a function that processes an object of type T and returns an error.
type NotificationHandler[T any] func(template T, actorAccountRef primitive.ObjectID) error
// sinkNotification is the default no-op strategy.
func sinkNotification[T any](_ T, _ primitive.ObjectID) error {
return nil
}
// NotificationConfig manages notifications for Create, Update, and Delete operations.
type NotificationConfig[T any] struct {
producer messaging.Producer
// The factory now receives a NotificationAction so it knows which event is being processed.
factory func(template T, actorAccountRef primitive.ObjectID, t model.NotificationAction) notifications.Envelope
CreateNotification NotificationHandler[T]
UpdateNotification NotificationHandler[T]
NeedArchiveNotification bool
ArchiveNotification NotificationHandler[T]
NeedDeleteNotification bool
DeleteNotification NotificationHandler[T]
}
// NewNotificationConfig creates a new NotificationConfig with default (no-op) strategies.
func NewNotificationConfig[T any](producer messaging.Producer) *NotificationConfig[T] {
return &NotificationConfig[T]{
producer: producer,
factory: nil, // no factory by default
CreateNotification: sinkNotification[T],
UpdateNotification: sinkNotification[T],
ArchiveNotification: sinkNotification[T],
NeedArchiveNotification: false,
DeleteNotification: sinkNotification[T],
NeedDeleteNotification: false,
}
}
// WithNotifications sets the notification factory and switches all endpoints to the sending strategy.
func (nc *NotificationConfig[T]) WithNotifications(factory func(template T, actorAccountRef primitive.ObjectID, typ model.NotificationAction) notifications.Envelope) *NotificationConfig[T] {
nc.factory = factory
// Build sending functions for each notification type.
nc.CreateNotification = func(template T, actorAccountRef primitive.ObjectID) error {
return nc.producer.SendMessage(factory(template, actorAccountRef, model.NACreated))
}
nc.UpdateNotification = func(template T, actorAccountRef primitive.ObjectID) error {
return nc.producer.SendMessage(factory(template, actorAccountRef, model.NAUpdated))
}
nc.ArchiveNotification = func(template T, actorAccountRef primitive.ObjectID) error {
return nc.producer.SendMessage(factory(template, actorAccountRef, model.NAArchived))
}
nc.NeedArchiveNotification = true
nc.DeleteNotification = func(template T, actorAccountRef primitive.ObjectID) error {
return nc.producer.SendMessage(factory(template, actorAccountRef, model.NADeleted))
}
nc.NeedDeleteNotification = true
return nc
}
// WithNoCreateNotification disables the create notification.
func (nc *NotificationConfig[T]) WithNoCreateNotification() *NotificationConfig[T] {
nc.CreateNotification = sinkNotification[T]
return nc
}
// WithNoUpdateNotification disables the update notification.
func (nc *NotificationConfig[T]) WithNoUpdateNotification() *NotificationConfig[T] {
nc.UpdateNotification = sinkNotification[T]
return nc
}
func (nc *NotificationConfig[T]) WithNoArchiveNotification() *NotificationConfig[T] {
nc.ArchiveNotification = sinkNotification[T]
return nc
}
// WithNoDeleteNotification disables the delete notification.
func (nc *NotificationConfig[T]) WithNoDeleteNotification() *NotificationConfig[T] {
nc.DeleteNotification = sinkNotification[T]
nc.NeedDeleteNotification = false
return nc
}

View File

@@ -0,0 +1,33 @@
package papitemplate
import (
"encoding/json"
"net/http"
"github.com/tech/sendico/pkg/db/repository"
"github.com/tech/sendico/pkg/db/repository/builder"
"github.com/tech/sendico/server/interface/api/srequest"
)
type ReorderRequestProcessor func(r *http.Request) (*srequest.ReorderX, builder.Query, error)
type ReorderConfig struct {
DB ReorderDB
ReqProcessor ReorderRequestProcessor
}
func (cfg *PAPIConfig) WithReorderHandler(reorder ReorderConfig) *PAPIConfig {
cfg.Reorder = &reorder
if cfg.Reorder.ReqProcessor == nil {
cfg.Reorder.ReqProcessor = defaultRequestProcessor
}
return cfg
}
func defaultRequestProcessor(r *http.Request) (*srequest.ReorderX, builder.Query, error) {
var req srequest.ReorderXDefault
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, nil, err
}
return &req.ReorderX, repository.OrgFilter(req.ParentRef), nil
}

View File

@@ -0,0 +1,33 @@
package papitemplate
import (
"context"
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
"github.com/tech/sendico/server/interface/api/sresponse"
"go.uber.org/zap"
)
func (a *ProtectedAPI[T]) reorder(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc {
a.Logger.Debug("Processing reorder request...")
req, filter, err := a.config.Reorder.ReqProcessor(r)
if err != nil {
a.Logger.Warn("Failed to decode tasks reorder request", zap.Error(err), mzap.StorableRef(account))
return response.BadPayload(a.Logger, a.Name(), err)
}
a.Logger.Debug("Moving objects", mzap.ObjRef("object_ref", req.ObjectRef), zap.Int("new_index", req.To))
if _, err := a.a.DBFactory().TransactionFactory().CreateTransaction().Execute(r.Context(), func(ctx context.Context) (any, error) {
// reorder is not atomic, so wrappping into transaction
return nil, a.config.Reorder.DB.Reorder(ctx, account.ID, req.ObjectRef, req.To, filter)
}); err != nil {
a.Logger.Warn("Failed to reorder tasks", zap.Error(err), mzap.ObjRef("object_ref", req.ObjectRef), zap.Int("to", req.To))
return response.Auto(a.Logger, a.Name(), err)
}
a.Logger.Debug("Reorder request processing complete")
return response.Success(a.Logger)
}

View File

@@ -0,0 +1,19 @@
package papitemplate
import (
"net/http"
"github.com/tech/sendico/server/interface/api/sresponse"
)
func (a *ProtectedAPI[T]) Objects(items []T, accessToken *sresponse.TokenData) http.HandlerFunc {
return sresponse.ObjectsAuth(a.Logger, items, accessToken, a.Name())
}
func (a *ProtectedAPI[T]) Object(item *T, accessToken *sresponse.TokenData) http.HandlerFunc {
return sresponse.ObjectAuth(a.Logger, item, accessToken, a.Name())
}
func (a *ProtectedAPI[T]) ObjectCreated(item *T, accessToken *sresponse.TokenData) http.HandlerFunc {
return sresponse.ObjectAuthCreated(a.Logger, item, accessToken, a.Name())
}

View File

@@ -0,0 +1,203 @@
package papitemplate
import (
"context"
api "github.com/tech/sendico/pkg/api/http"
notifications "github.com/tech/sendico/pkg/messaging/envelope"
"github.com/tech/sendico/pkg/mlogger"
model "github.com/tech/sendico/pkg/model/notification"
"github.com/tech/sendico/pkg/mservice"
eapi "github.com/tech/sendico/server/interface/api"
"github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
type ProtectedAPI[T any] struct {
Logger mlogger.Logger
DB ProtectedDB[T]
Oph mutil.ParamHelper // org param handler
Pph mutil.ParamHelper // parent object param handler
Cph mutil.ParamHelper // child object param handler
resource mservice.Type
a eapi.API
config *PAPIConfig
nconfig *NotificationConfig[*T]
}
func (a *ProtectedAPI[_]) Name() mservice.Type {
return a.resource
}
func (_ *ProtectedAPI[_]) Finish(_ context.Context) error {
return nil
}
func (a *ProtectedAPI[T]) Build() *ProtectedAPI[T] {
createHandler := a.config.CreateResolver(a.create)
if createHandler != nil {
a.a.Register().AccountHandler(a.Name(), a.Oph.AddRef("/"), api.Post, createHandler)
}
listHandler := a.config.ListResolver(a.list)
if listHandler != nil {
a.a.Register().AccountHandler(a.Name(), a.Pph.AddRef(a.Oph.AddRef("/list")), api.Get, listHandler)
}
getHandler := a.config.GetResolver(a.get)
if getHandler != nil {
a.a.Register().AccountHandler(a.Name(), a.Cph.AddRef("/"), api.Get, getHandler)
}
updateHandler := a.config.UpdateResolver(a.update)
if updateHandler != nil {
a.a.Register().AccountHandler(a.Name(), "/", api.Put, updateHandler)
}
deleteHandler := a.config.DeleteResolver(a.delete)
if deleteHandler != nil {
a.a.Register().AccountHandler(a.Name(), a.Cph.AddRef("/"), api.Delete, deleteHandler)
}
archiveHandler := a.config.ArchiveResolver(a.archive)
if archiveHandler != nil {
a.a.Register().AccountHandler(a.Name(), a.Cph.AddRef(a.Oph.AddRef("/archive")), api.Get, archiveHandler)
}
if a.config.Reorder != nil {
a.a.Register().AccountHandler(a.Name(), "/reorder", api.Post, a.reorder)
}
if a.config.Taggable != nil {
a.a.Register().AccountHandler(a.Name(), "/tags/add", api.Put, a.addTag)
a.a.Register().AccountHandler(a.Name(), "/tags/add", api.Post, a.addTags)
a.a.Register().AccountHandler(a.Name(), "/tags", api.Delete, a.removeTag)
a.a.Register().AccountHandler(a.Name(), "/tags/all", api.Delete, a.removeAllTags)
a.a.Register().AccountHandler(a.Name(), "/tags/set", api.Post, a.setTags)
a.a.Register().AccountHandler(a.Name(), "/tags", api.Get, a.getTags)
}
return a
}
func (a *ProtectedAPI[T]) WithNotifications(factory func(template *T, actorAccountRef primitive.ObjectID, t model.NotificationAction) notifications.Envelope) *ProtectedAPI[T] {
a.nconfig.WithNotifications(factory)
a.Logger.Info("Notificatons handler installed")
return a
}
// WithNoCreateNotification disables the create notification.
func (a *ProtectedAPI[T]) WithNoCreateNotification() *ProtectedAPI[T] {
a.nconfig.WithNoCreateNotification()
a.Logger.Info("Object creation notificaton disabled")
return a
}
// WithNoUpdateNotification disables the update notification.
func (a *ProtectedAPI[T]) WithNoUpdateNotification() *ProtectedAPI[T] {
a.nconfig.WithNoUpdateNotification()
a.Logger.Info("Object update notificaton disabled")
return a
}
// WithNoDeleteNotification disables the delete notification.
func (a *ProtectedAPI[T]) WithNoDeleteNotification() *ProtectedAPI[T] {
a.nconfig.WithNoDeleteNotification()
a.Logger.Info("Object deletion notificaton disabled")
return a
}
func (a *ProtectedAPI[T]) WithNoCreate() *ProtectedAPI[T] {
a.config.WithNoCreate()
a.Logger.Info("Create handler disabled")
return a
}
func (a *ProtectedAPI[T]) WithCreateHandler(handler sresponse.AccountHandlerFunc) *ProtectedAPI[T] {
a.config.WithCreateHandler(handler)
a.Logger.Info("Create handler overridden")
return a
}
func (a *ProtectedAPI[T]) WithNoList() *ProtectedAPI[T] {
a.config.WithNoList()
a.Logger.Info("List handler disabled")
return a
}
func (a *ProtectedAPI[T]) WithListHandler(handler sresponse.AccountHandlerFunc) *ProtectedAPI[T] {
a.config.WithListHandler(handler)
a.Logger.Info("List handler overridden")
return a
}
func (a *ProtectedAPI[T]) WithNoGet() *ProtectedAPI[T] {
a.config.WithNoGet()
a.Logger.Info("Get handler disabled")
return a
}
func (a *ProtectedAPI[T]) WithGetHandler(handler sresponse.AccountHandlerFunc) *ProtectedAPI[T] {
a.config.WithGetHandler(handler)
a.Logger.Info("Get handler overridden")
return a
}
func (a *ProtectedAPI[T]) WithReorderHandler(reorder ReorderConfig) *ProtectedAPI[T] {
a.config.WithReorderHandler(reorder)
a.Logger.Info("Reorder handler installed")
return a
}
func (a *ProtectedAPI[T]) WithTaggableHandler(taggable TaggableConfig) *ProtectedAPI[T] {
a.config.WithTaggableHandler(taggable)
a.Logger.Info("Taggable handlers installed")
return a
}
func (a *ProtectedAPI[T]) WithNoUpdate() *ProtectedAPI[T] {
a.config.WithNoUpdate()
a.Logger.Info("Update handler disabled")
return a
}
func (a *ProtectedAPI[T]) WithUpdateHandler(handler sresponse.AccountHandlerFunc) *ProtectedAPI[T] {
a.config.WithUpdateHandler(handler)
a.Logger.Info("Update handler overridden")
return a
}
func (a *ProtectedAPI[T]) WithNoDelete() *ProtectedAPI[T] {
a.config.WithNoDelete()
a.Logger.Info("Delete handler disabled")
return a
}
func (a *ProtectedAPI[T]) WithDeleteHandler(handler sresponse.AccountHandlerFunc) *ProtectedAPI[T] {
a.config.WithDeleteHandler(handler)
a.Logger.Info("Delete handler overriden")
return a
}
func CreateAPI[T any](a eapi.API, dbFactory func() (ProtectedDB[T], error), parent, resource mservice.Type) (*ProtectedAPI[T], error) {
p := &ProtectedAPI[T]{
Logger: a.Logger().Named(resource),
Oph: mutil.CreatePH("org"), // to avoid collision with organizaitons_ref when
Pph: mutil.CreatePH(parent),
resource: resource,
Cph: mutil.CreatePH(resource),
a: a,
config: NewConfig(),
nconfig: NewNotificationConfig[*T](a.Register().Messaging().Producer()),
}
var err error
if p.DB, err = dbFactory(); err != nil {
p.Logger.Error("Failed to create protected database", zap.Error(err))
return nil, err
}
return p, nil
}

View File

@@ -0,0 +1,122 @@
package papitemplate
import (
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
"github.com/tech/sendico/server/interface/api/sresponse"
"go.uber.org/zap"
)
func (a *ProtectedAPI[T]) addTag(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc {
a.Logger.Debug("Processing add tag request...")
req, err := a.config.Taggable.AddTagReqProcessor(r)
if err != nil {
a.Logger.Warn("Failed to decode add tag request", zap.Error(err), mzap.StorableRef(account))
return response.BadPayload(a.Logger, a.Name(), err)
}
a.Logger.Debug("Adding tag to object", mzap.ObjRef("object_ref", req.ObjectRef), mzap.ObjRef("tag_ref", req.TagRef))
if err := a.config.Taggable.DB.AddTag(r.Context(), account.ID, req.ObjectRef, req.TagRef); err != nil {
a.Logger.Warn("Failed to add tag to object", zap.Error(err), mzap.ObjRef("object_ref", req.ObjectRef), mzap.ObjRef("tag_ref", req.TagRef))
return response.Auto(a.Logger, a.Name(), err)
}
a.Logger.Debug("Add tag request processing complete")
return response.Success(a.Logger)
}
func (a *ProtectedAPI[T]) addTags(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc {
a.Logger.Debug("Processing add tags request...")
req, err := a.config.Taggable.AddTagsReqProcessor(r)
if err != nil {
a.Logger.Warn("Failed to decode add tags request", zap.Error(err), mzap.StorableRef(account))
return response.BadPayload(a.Logger, a.Name(), err)
}
a.Logger.Debug("Adding tags to object", mzap.ObjRef("object_ref", req.ObjectRef), zap.Int("tag_count", len(req.TagRefs)))
if err := a.config.Taggable.DB.AddTags(r.Context(), account.ID, req.ObjectRef, req.TagRefs); err != nil {
a.Logger.Warn("Failed to add tags to object", zap.Error(err), mzap.ObjRef("object_ref", req.ObjectRef))
return response.Auto(a.Logger, a.Name(), err)
}
a.Logger.Debug("Add tags request processing complete")
return response.Success(a.Logger)
}
func (a *ProtectedAPI[T]) removeTag(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc {
a.Logger.Debug("Processing remove tag request...")
req, err := a.config.Taggable.RemoveTagReqProcessor(r)
if err != nil {
a.Logger.Warn("Failed to decode remove tag request", zap.Error(err), mzap.StorableRef(account))
return response.BadPayload(a.Logger, a.Name(), err)
}
a.Logger.Debug("Removing tag from object", mzap.ObjRef("object_ref", req.ObjectRef), mzap.ObjRef("tag_ref", req.TagRef))
if err := a.config.Taggable.DB.RemoveTag(r.Context(), account.ID, req.ObjectRef, req.TagRef); err != nil {
a.Logger.Warn("Failed to remove tag from object", zap.Error(err), mzap.ObjRef("object_ref", req.ObjectRef), mzap.ObjRef("tag_ref", req.TagRef))
return response.Auto(a.Logger, a.Name(), err)
}
a.Logger.Debug("Remove tag request processing complete")
return response.Success(a.Logger)
}
func (a *ProtectedAPI[T]) removeAllTags(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc {
a.Logger.Debug("Processing remove all tags request...")
req, err := a.config.Taggable.RemoveAllTagsReqProcessor(r)
if err != nil {
a.Logger.Warn("Failed to decode remove all tags request", zap.Error(err), mzap.StorableRef(account))
return response.BadPayload(a.Logger, a.Name(), err)
}
a.Logger.Debug("Removing all tags from object", mzap.ObjRef("object_ref", req.ObjectRef))
if err := a.config.Taggable.DB.RemoveAllTags(r.Context(), account.ID, req.ObjectRef); err != nil {
a.Logger.Warn("Failed to remove all tags from object", zap.Error(err), mzap.ObjRef("object_ref", req.ObjectRef))
return response.Auto(a.Logger, a.Name(), err)
}
a.Logger.Debug("Remove all tags request processing complete")
return response.Success(a.Logger)
}
func (a *ProtectedAPI[T]) setTags(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc {
a.Logger.Debug("Processing set tags request...")
req, err := a.config.Taggable.SetTagsReqProcessor(r)
if err != nil {
a.Logger.Warn("Failed to decode set tags request", zap.Error(err), mzap.StorableRef(account))
return response.BadPayload(a.Logger, a.Name(), err)
}
a.Logger.Debug("Setting tags for object", mzap.ObjRef("object_ref", req.ObjectRef), zap.Int("tag_count", len(req.TagRefs)))
if err := a.config.Taggable.DB.SetTags(r.Context(), account.ID, req.ObjectRef, req.TagRefs); err != nil {
a.Logger.Warn("Failed to set tags for object", zap.Error(err), mzap.ObjRef("object_ref", req.ObjectRef))
return response.Auto(a.Logger, a.Name(), err)
}
a.Logger.Debug("Set tags request processing complete")
return response.Success(a.Logger)
}
func (a *ProtectedAPI[T]) getTags(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc {
a.Logger.Debug("Processing get tags request...")
req, err := a.config.Taggable.GetTagsReqProcessor(r)
if err != nil {
a.Logger.Warn("Failed to decode get tags request", zap.Error(err), mzap.StorableRef(account))
return response.BadPayload(a.Logger, a.Name(), err)
}
a.Logger.Debug("Getting tags for object", mzap.ObjRef("object_ref", req.ObjectRef))
tagRefs, err := a.config.Taggable.DB.GetTags(r.Context(), account.ID, req.ObjectRef)
if err != nil {
a.Logger.Warn("Failed to get tags for object", zap.Error(err), mzap.ObjRef("object_ref", req.ObjectRef))
return response.Auto(a.Logger, a.Name(), err)
}
a.Logger.Debug("Get tags request processing complete", zap.Int("tag_count", len(tagRefs)))
return response.Ok(a.Logger, map[string]interface{}{
"tagRefs": tagRefs,
})
}

View File

@@ -0,0 +1,80 @@
package papitemplate
import (
"context"
"encoding/json"
"net/http"
"github.com/tech/sendico/server/interface/api/srequest"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type (
TaggableSingleRequestProcessor func(r *http.Request) (*srequest.TaggableSingle, error)
TaggableMultipleRequestProcessor func(r *http.Request) (*srequest.TaggableMultiple, error)
TaggableObjectRequestProcessor func(r *http.Request) (*srequest.TaggableObject, error)
)
// TaggableDB interface defines the required methods for tag operations
type TaggableDB interface {
AddTag(ctx context.Context, accountRef, objectRef, tagRef primitive.ObjectID) error
AddTags(ctx context.Context, accountRef, objectRef primitive.ObjectID, tagRefs []primitive.ObjectID) error
RemoveTag(ctx context.Context, accountRef, objectRef, tagRef primitive.ObjectID) error
RemoveAllTags(ctx context.Context, accountRef, objectRef primitive.ObjectID) error
SetTags(ctx context.Context, accountRef, objectRef primitive.ObjectID, tagRefs []primitive.ObjectID) error
GetTags(ctx context.Context, accountRef, objectRef primitive.ObjectID) ([]primitive.ObjectID, error)
}
type TaggableConfig struct {
DB TaggableDB
AddTagReqProcessor TaggableSingleRequestProcessor
AddTagsReqProcessor TaggableMultipleRequestProcessor
RemoveTagReqProcessor TaggableSingleRequestProcessor
RemoveAllTagsReqProcessor TaggableObjectRequestProcessor
SetTagsReqProcessor TaggableMultipleRequestProcessor
GetTagsReqProcessor TaggableObjectRequestProcessor
}
func (cfg *PAPIConfig) WithTaggableHandler(taggable TaggableConfig) *PAPIConfig {
cfg.Taggable = &taggable
if cfg.Taggable.AddTagReqProcessor == nil {
cfg.Taggable.AddTagReqProcessor = defaultTaggableSingleRequestProcessor
}
if cfg.Taggable.AddTagsReqProcessor == nil {
cfg.Taggable.AddTagsReqProcessor = defaultTaggableMultipleRequestProcessor
}
if cfg.Taggable.RemoveTagReqProcessor == nil {
cfg.Taggable.RemoveTagReqProcessor = defaultTaggableSingleRequestProcessor
}
if cfg.Taggable.RemoveAllTagsReqProcessor == nil {
cfg.Taggable.RemoveAllTagsReqProcessor = defaultTaggableObjectRequestProcessor
}
if cfg.Taggable.SetTagsReqProcessor == nil {
cfg.Taggable.SetTagsReqProcessor = defaultTaggableMultipleRequestProcessor
}
return cfg
}
func defaultTaggableSingleRequestProcessor(r *http.Request) (*srequest.TaggableSingle, error) {
var req srequest.TaggableSingle
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
}
return &req, nil
}
func defaultTaggableMultipleRequestProcessor(r *http.Request) (*srequest.TaggableMultiple, error) {
var req srequest.TaggableMultiple
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
}
return &req, nil
}
func defaultTaggableObjectRequestProcessor(r *http.Request) (*srequest.TaggableObject, error) {
var req srequest.TaggableObject
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
}
return &req, nil
}

View File

@@ -0,0 +1,31 @@
package papitemplate
import (
"encoding/json"
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
"github.com/tech/sendico/server/interface/api/sresponse"
"go.uber.org/zap"
)
func (a *ProtectedAPI[T]) update(r *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
var object T
if err := json.NewDecoder(r.Body).Decode(&object); err != nil {
a.Logger.Warn("Failed to decode object when updating settings", zap.Error(err), mzap.StorableRef(account))
return response.BadPayload(a.Logger, a.Name(), err)
}
if err := a.DB.Update(r.Context(), *account.GetID(), &object); err != nil {
a.Logger.Warn("Error updating object", zap.Error(err), mzap.StorableRef(account))
return response.Auto(a.Logger, a.Name(), err)
}
if err := a.nconfig.UpdateNotification(&object, *account.GetID()); err != nil {
a.Logger.Warn("Failed to send creation notification", zap.Error(err))
}
return a.Object(&object, accessToken)
}