service backend
This commit is contained in:
84
api/pkg/model/account.go
Executable file
84
api/pkg/model/account.go
Executable file
@@ -0,0 +1,84 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type Filter int
|
||||
|
||||
type AccountBase struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
ArchivableBase `bson:",inline" json:",inline"`
|
||||
Describable `bson:",inline" json:",inline"`
|
||||
AvatarURL *string `bson:"avatarUrl,omitempty" json:"avatarUrl,omitempty"`
|
||||
}
|
||||
|
||||
func (*AccountBase) Collection() string {
|
||||
return mservice.Accounts
|
||||
}
|
||||
|
||||
type AccountPublic struct {
|
||||
AccountBase `bson:",inline" json:",inline"`
|
||||
UserDataBase `bson:",inline" json:",inline"`
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
AccountPublic `bson:",inline" json:",inline"`
|
||||
EmailBackup string `bson:"emailBackup" json:"emailBackup"`
|
||||
Password string `bson:"password" json:"password"`
|
||||
ResetPasswordToken string `bson:"resetPasswordToken" json:"resetPasswordToken"`
|
||||
VerifyToken string `bson:"verifyToken" json:"verifyToken"`
|
||||
}
|
||||
|
||||
func (a *Account) HashPassword() error {
|
||||
key, err := bcrypt.GenerateFromPassword([]byte(a.Password), bcrypt.DefaultCost)
|
||||
if err == nil {
|
||||
a.Password = string(key)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *Account) MatchPassword(password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(a.Password), []byte(password))
|
||||
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func AnonymousUserName(orgRef primitive.ObjectID) string {
|
||||
return "anonymous@" + orgRef.Hex()
|
||||
}
|
||||
|
||||
func AccountIsAnonymous(account *UserDataBase, orgRef primitive.ObjectID) bool {
|
||||
if account == nil {
|
||||
return false
|
||||
}
|
||||
return AnonymousUserName(orgRef) == account.Login
|
||||
}
|
||||
|
||||
type AccountBound interface {
|
||||
GetAccountRef() primitive.ObjectID
|
||||
}
|
||||
|
||||
type AccountBoundStorable interface {
|
||||
storable.Storable
|
||||
OrganizationBound
|
||||
GetAccountRef() *primitive.ObjectID
|
||||
}
|
||||
|
||||
const (
|
||||
AccountRefField = "accountRef"
|
||||
)
|
||||
|
||||
type AccountBoundBase struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
OrganizationBoundBase `bson:",inline" json:",inline"`
|
||||
AccountRef *primitive.ObjectID `bson:"accountRef,omitempty" json:"accountRef,omitempty"`
|
||||
}
|
||||
|
||||
func (a *AccountBoundBase) GetAccountRef() *primitive.ObjectID {
|
||||
return a.AccountRef
|
||||
}
|
||||
5
api/pkg/model/ampli.go
Normal file
5
api/pkg/model/ampli.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package model
|
||||
|
||||
type AmpliEvent struct {
|
||||
UserID string
|
||||
}
|
||||
18
api/pkg/model/archivable.go
Normal file
18
api/pkg/model/archivable.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package model
|
||||
|
||||
type Archivable interface {
|
||||
IsArchived() bool
|
||||
SetArchived(archived bool)
|
||||
}
|
||||
|
||||
type ArchivableBase struct {
|
||||
Archived bool `bson:"isArchived" json:"isArchived"`
|
||||
}
|
||||
|
||||
func (a *ArchivableBase) IsArchived() bool {
|
||||
return a.Archived
|
||||
}
|
||||
|
||||
func (a *ArchivableBase) SetArchived(archived bool) {
|
||||
a.Archived = archived
|
||||
}
|
||||
8
api/pkg/model/attachment.go
Normal file
8
api/pkg/model/attachment.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package model
|
||||
|
||||
// Attachment represents metadata for an attachment in a comment.
|
||||
type Attachment struct {
|
||||
Describable `bson:",inline" json:",inline"`
|
||||
Type string `bson:"type" json:"type"` // Type of attachment (e.g., "image", "file", "rich_text")
|
||||
URL string `bson:"url" json:"url"` // URL of the attachment (e.g., an image or file location)
|
||||
}
|
||||
84
api/pkg/model/auth.go
Normal file
84
api/pkg/model/auth.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// Action represents a permissible action on a resource.
|
||||
type Action string
|
||||
|
||||
// Common actions for resources.
|
||||
const (
|
||||
ActionCreate Action = "create" // Create a resource
|
||||
ActionRead Action = "read" // Read or view a resource
|
||||
ActionUpdate Action = "update" // Update or modify a resource
|
||||
ActionDelete Action = "delete" // Delete a resource
|
||||
)
|
||||
|
||||
// Effect determines whether an action is allowed or denied.
|
||||
type Effect string
|
||||
|
||||
const (
|
||||
EffectAllow Effect = "allow" // Permit the action
|
||||
EffectDeny Effect = "deny" // Deny the action
|
||||
)
|
||||
|
||||
// RoleDescription provides metadata about a role.
|
||||
type RoleDescription struct {
|
||||
storable.Base `bson:",inline" json:",inline"` // Base fields for MongoDB documents
|
||||
Describable `bson:",inline" json:",inline"` // Name and description fields
|
||||
OrganizationRef primitive.ObjectID `bson:"organizationRef" json:"organizationRef"` // Organization associated with the role
|
||||
}
|
||||
|
||||
// Collection specifies the MongoDB collection for RoleDescription.
|
||||
func (*RoleDescription) Collection() string {
|
||||
return mservice.Roles
|
||||
}
|
||||
|
||||
// Role represents a role assignment for an account within an organization.
|
||||
type Role struct {
|
||||
AccountRef primitive.ObjectID `bson:"accountRef" json:"accountRef"` // Account assigned to the role
|
||||
DescriptionRef primitive.ObjectID `bson:"descriptionRef" json:"descriptionRef"` // Reference to the role's description
|
||||
OrganizationRef primitive.ObjectID `bson:"organizationRef" json:"organizationRef"` // Organization where the role is applicable
|
||||
}
|
||||
|
||||
// ActionEffect represents a combination of an action and its effect (allow/deny).
|
||||
type ActionEffect struct {
|
||||
Action Action `bson:"action" json:"action"` // The action to perform (e.g., read, write)
|
||||
Effect Effect `bson:"effect" json:"effect"` // Whether the action is allowed or denied
|
||||
}
|
||||
|
||||
// Policy defines access control rules for a role within an organization.
|
||||
type Policy struct {
|
||||
OrganizationRef primitive.ObjectID `bson:"organizationRef" json:"organizationRef"` // Organization associated with the policy
|
||||
DescriptionRef primitive.ObjectID `bson:"descriptionRef" json:"descriptionRef"` // Reference to the policy's metadata
|
||||
ObjectRef *primitive.ObjectID `bson:"objectRef,omitempty" json:"objectRef,omitempty"` // Target object (NilObjectID for all objects)
|
||||
Effect ActionEffect `bson:"effect" json:"effect"` // Action and effect for the policy
|
||||
}
|
||||
|
||||
// RolePolicy defines access control rules for a role within an organization.
|
||||
type RolePolicy struct {
|
||||
Policy `bson:",inline" json:",inline"`
|
||||
RoleDescriptionRef primitive.ObjectID `bson:"roleDescriptionRef" json:"roleDescriptionRef"` // Reference to the associated role
|
||||
}
|
||||
|
||||
// PolicyDescription provides metadata for policies.
|
||||
type PolicyDescription struct {
|
||||
storable.Base `bson:",inline" json:",inline"` // Base fields for MongoDB documents
|
||||
Describable `bson:",inline" json:",inline"` // Name and description fields
|
||||
ResourceTypes *[]mservice.Type `bson:"resourceTypes,omitempty" json:"resourceTypes,omitempty"` // nil for custom policies, non-nil for built-in permissisons
|
||||
OrganizationRef *primitive.ObjectID `bson:"organizationRef,omitempty" json:"organizationRef,omitempty"` // nil for built-in policies, non-nil for custom
|
||||
}
|
||||
|
||||
// Collection specifies the MongoDB collection for PolicyDescription.
|
||||
func (*PolicyDescription) Collection() string {
|
||||
return mservice.Policies
|
||||
}
|
||||
|
||||
// Permission ties a policy to a specific account.
|
||||
type Permission struct {
|
||||
RolePolicy `bson:",inline" json:",inline"` // Embedded policy definition
|
||||
AccountRef primitive.ObjectID `bson:"accountRef" json:"accountRef"` // Account assigned the permission
|
||||
}
|
||||
15
api/pkg/model/automation.go
Normal file
15
api/pkg/model/automation.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
)
|
||||
|
||||
type Automation struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
Describable `bson:",inline" json:",inline"`
|
||||
}
|
||||
|
||||
func (*Automation) Collection() string {
|
||||
return mservice.Automations
|
||||
}
|
||||
24
api/pkg/model/client.go
Normal file
24
api/pkg/model/client.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
ClientID string `bson:"clientId"`
|
||||
ClientName string `bson:"clientName"`
|
||||
ClientSecret string `bson:"clientSecret,omitempty"`
|
||||
AllowedScopes []string `bson:"allowedScopes"`
|
||||
RedirectURIs []string `bson:"redirectURIs"`
|
||||
GrantTypes []string `bson:"grantTypes"`
|
||||
TokenEndpointAuthMethod string `bson:"tokenEndpointAuthMethod"`
|
||||
AccountRef *primitive.ObjectID `bson:"accountRef,omitempty"` // owner reference
|
||||
IsRevoked bool `bson:"isRevoked"`
|
||||
}
|
||||
|
||||
func (*Client) Collection() string {
|
||||
return mservice.Clients
|
||||
}
|
||||
6
api/pkg/model/colorable.go
Normal file
6
api/pkg/model/colorable.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package model
|
||||
|
||||
type Colorable struct {
|
||||
Describable `bson:",inline" json:",inline"`
|
||||
Color *string `bson:"color,omitempty" json:"color,omitempty"` // Optional color (e.g., hex code for UI display)
|
||||
}
|
||||
35
api/pkg/model/comment.go
Normal file
35
api/pkg/model/comment.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type CommentBase struct {
|
||||
PermissionBound `bson:",inline" json:",inline"`
|
||||
AuthorRef primitive.ObjectID `bson:"authorRef" json:"authorRef"` // Reference to the author (user) of the comment
|
||||
TaskRef primitive.ObjectID `bson:"taskRef" json:"taskRef"` // Reference to the task
|
||||
Attachments []Attachment `bson:"attachments" json:"attachments"` // List of attachments
|
||||
Reactions []Reaction `bson:"reactions" json:"reactions"` // List of attachments
|
||||
Content string `bson:"content" json:"content"` // Text content
|
||||
IsFormatted bool `bson:"isFormatted" json:"isFormatted"` // Flag for formatted content
|
||||
}
|
||||
|
||||
func (*CommentBase) Collection() string {
|
||||
return mservice.Comments
|
||||
}
|
||||
|
||||
// Comment represents a comment attached to a task.
|
||||
type Comment struct {
|
||||
CommentBase `bson:",inline" json:",inline"`
|
||||
}
|
||||
|
||||
// NewTaskComment creates a new instance of TaskComment.
|
||||
func NewComment(taskRef, authorRef primitive.ObjectID, content string) *Comment {
|
||||
return &Comment{
|
||||
CommentBase: CommentBase{
|
||||
AuthorRef: authorRef,
|
||||
Content: content,
|
||||
},
|
||||
}
|
||||
}
|
||||
8
api/pkg/model/commentp.go
Normal file
8
api/pkg/model/commentp.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package model
|
||||
|
||||
import "go.mongodb.org/mongo-driver/bson/primitive"
|
||||
|
||||
type CommentPreview struct {
|
||||
TaskRef primitive.ObjectID `json:"taskRef" bson:"taskRef"`
|
||||
CommentsCount int `json:"commentsCount" bson:"commentsCount"`
|
||||
}
|
||||
27
api/pkg/model/currency.go
Normal file
27
api/pkg/model/currency.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package model
|
||||
|
||||
type Currency string
|
||||
|
||||
const (
|
||||
CurrencyEUR Currency = "EUR" // Euro
|
||||
CurrencyUSD Currency = "USD" // US Dollar
|
||||
CurrencyRUB Currency = "RUB" // Russian Ruble
|
||||
CurrencyUAH Currency = "UAH" // Ukrainian Hryvnia
|
||||
CurrencyPLN Currency = "PLN" // Polish Złoty
|
||||
CurrencyCZK Currency = "CZK" // Czech Koruna
|
||||
)
|
||||
|
||||
// All supported currencies
|
||||
var SupportedCurrencies = []Currency{
|
||||
CurrencyEUR,
|
||||
CurrencyUSD,
|
||||
CurrencyRUB,
|
||||
CurrencyUAH,
|
||||
CurrencyPLN,
|
||||
CurrencyCZK,
|
||||
}
|
||||
|
||||
type Amount struct {
|
||||
Total float64 `bson:"total" json:"total"` // Total amount billed
|
||||
Currency Currency `bson:"currency" json:"currency"` // Currency for the invoice
|
||||
}
|
||||
13
api/pkg/model/customizable.go
Normal file
13
api/pkg/model/customizable.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package model
|
||||
|
||||
type Custimizable interface {
|
||||
GetProperties() []Value
|
||||
}
|
||||
|
||||
type CustomozableBase struct {
|
||||
Properties []Value `bson:"properties" json:"properties"`
|
||||
}
|
||||
|
||||
func (c *CustomozableBase) GetProperties() []Value {
|
||||
return c.Properties
|
||||
}
|
||||
11
api/pkg/model/describable.go
Normal file
11
api/pkg/model/describable.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package model
|
||||
|
||||
type Describable struct {
|
||||
Name string `bson:"name" json:"name"` // Name
|
||||
Description *string `bson:"description,omitempty" json:"description,omitempty"` // Optional description
|
||||
}
|
||||
|
||||
const (
|
||||
NameField = "name"
|
||||
DescriptionField = "description"
|
||||
)
|
||||
7
api/pkg/model/dzone.go
Normal file
7
api/pkg/model/dzone.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package model
|
||||
|
||||
type DZone struct {
|
||||
CanDeleteAccount bool `json:"canDeleteAccount"`
|
||||
CanDeleteCascade bool `json:"canDeleteCascade"`
|
||||
Organizations []Organization `json:"organizations"`
|
||||
}
|
||||
8
api/pkg/model/fconfig.go
Normal file
8
api/pkg/model/fconfig.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package model
|
||||
|
||||
type SettingsT = map[string]any
|
||||
|
||||
type DriverConfig[T any] struct {
|
||||
Driver T `yaml:"driver"`
|
||||
Settings SettingsT `yaml:"settings"`
|
||||
}
|
||||
31
api/pkg/model/filter.go
Normal file
31
api/pkg/model/filter.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package model
|
||||
|
||||
import "go.mongodb.org/mongo-driver/bson/primitive"
|
||||
|
||||
type TagFilterMode string
|
||||
|
||||
const (
|
||||
TagFilterModeNone TagFilterMode = "none"
|
||||
TagFilterModePresent TagFilterMode = "present"
|
||||
TagFilterModeMissing TagFilterMode = "missing"
|
||||
TagFilterModeIncludeAny TagFilterMode = "includeAny"
|
||||
TagFilterModeIncludeAll TagFilterMode = "includeAll"
|
||||
TagFilterModeExcludeAny TagFilterMode = "excludeAny"
|
||||
)
|
||||
|
||||
type TagFilter struct {
|
||||
Mode *TagFilterMode `bson:"mode,omitempty" json:"mode,omitempty"`
|
||||
TagRefs []primitive.ObjectID `bson:"tagRefs,omitempty" json:"tagRefs,omitempty"`
|
||||
}
|
||||
|
||||
type ObjectsFilter struct {
|
||||
Query *string `bson:"query,omitempty" json:"query,omitempty"`
|
||||
CaseSensitive *bool `bson:"caseSensitive,omitempty" json:"caseSensitive,omitempty"`
|
||||
TagFilter *TagFilter `bson:"tagFilter,omitempty" json:"tagFilter,omitempty"`
|
||||
Sort *ObjectsSort `bson:"sort,omitempty" json:"sort,omitempty"`
|
||||
}
|
||||
|
||||
type ObjectsSort struct {
|
||||
Field string `bson:"field" json:"field"`
|
||||
Direction string `bson:"direction" json:"direction"`
|
||||
}
|
||||
12
api/pkg/model/indexable.go
Normal file
12
api/pkg/model/indexable.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
import "github.com/tech/sendico/pkg/db/storable"
|
||||
|
||||
type Indexable struct {
|
||||
Index int `bson:"index" json:"index"`
|
||||
}
|
||||
|
||||
type IndexableRef struct {
|
||||
storable.Ref `bson:",inline" json:",inline"`
|
||||
Indexable `bson:",inline" json:",inline"`
|
||||
}
|
||||
83
api/pkg/model/internal/notificationevent.go
Normal file
83
api/pkg/model/internal/notificationevent.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
nm "github.com/tech/sendico/pkg/model/notification"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
)
|
||||
|
||||
type NotificationEventImp struct {
|
||||
nType mservice.Type
|
||||
nAction nm.NotificationAction
|
||||
}
|
||||
|
||||
func (ne *NotificationEventImp) GetType() mservice.Type {
|
||||
return ne.nType
|
||||
}
|
||||
|
||||
func (ne *NotificationEventImp) GetAction() nm.NotificationAction {
|
||||
return ne.nAction
|
||||
}
|
||||
|
||||
const messageDelimiter string = "_"
|
||||
|
||||
func (ne *NotificationEventImp) Equals(other *NotificationEventImp) bool {
|
||||
return (other != nil) && (ne.nType == other.nType) && (ne.nAction == other.nAction)
|
||||
}
|
||||
|
||||
func (ne *NotificationEventImp) ToString() string {
|
||||
return ne.StringType() + messageDelimiter + ne.StringAction()
|
||||
}
|
||||
|
||||
func (ne *NotificationEventImp) StringType() string {
|
||||
return string(ne.nType)
|
||||
}
|
||||
|
||||
func (ne *NotificationEventImp) StringAction() string {
|
||||
return string(ne.nAction)
|
||||
}
|
||||
|
||||
func NewNotificationImp(t mservice.Type, a nm.NotificationAction) *NotificationEventImp {
|
||||
return &NotificationEventImp{nType: t, nAction: a}
|
||||
}
|
||||
|
||||
func FromStringImp(s string) (*NotificationEventImp, error) {
|
||||
parts := strings.Split(s, messageDelimiter)
|
||||
if len(parts) != 2 {
|
||||
return nil, merrors.Internal("invalid_notification_event_format")
|
||||
}
|
||||
|
||||
res := &NotificationEventImp{}
|
||||
var err error
|
||||
if res.nType, err = mservice.StringToSType(parts[0]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.nAction, err = StringToNotificationAction(parts[1]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func StringToNotificationAction(s string) (nm.NotificationAction, error) {
|
||||
switch nm.NotificationAction(s) {
|
||||
case nm.NACreated, nm.NAPending, nm.NAUpdated, nm.NADeleted, nm.NAAssigned, nm.NAPasswordReset:
|
||||
return nm.NotificationAction(s), nil
|
||||
default:
|
||||
return "", merrors.DataConflict("invalid Notification action: " + s)
|
||||
}
|
||||
}
|
||||
|
||||
func StringToNotificationEventImp(eventType, eventAction string) (*NotificationEventImp, error) {
|
||||
et, err := mservice.StringToSType(eventType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ea, err := StringToNotificationAction(eventAction)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewNotificationImp(et, ea), nil
|
||||
}
|
||||
71
api/pkg/model/invitation.go
Normal file
71
api/pkg/model/invitation.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type InvitationStatus string
|
||||
|
||||
const (
|
||||
InvitationCreated InvitationStatus = "created"
|
||||
InvitationSent InvitationStatus = "sent"
|
||||
InvitationAccepted InvitationStatus = "accepted"
|
||||
InvitationDeclined InvitationStatus = "declined"
|
||||
InvitationRevoked InvitationStatus = "revoked"
|
||||
)
|
||||
|
||||
type invitationDesc struct {
|
||||
Email string `bson:"email" json:"email"`
|
||||
Name string `bson:"name" json:"name"`
|
||||
Comment string `bson:"comment" json:"comment"`
|
||||
}
|
||||
|
||||
func (id *invitationDesc) Collection() string {
|
||||
return mservice.Invitations
|
||||
}
|
||||
|
||||
type Invitation struct {
|
||||
PermissionBound `bson:",inline" json:",inline"`
|
||||
OrganizationRef primitive.ObjectID `bson:"organizationRef" json:"organizationRef"`
|
||||
RoleRef primitive.ObjectID `bson:"roleRef" json:"roleRef"`
|
||||
InviterRef primitive.ObjectID `bson:"inviterRef" json:"inviterRef"`
|
||||
Status InvitationStatus `bson:"status" json:"status"`
|
||||
ExpiresAt time.Time `bson:"expiresAt" json:"expiresAt"`
|
||||
Content invitationDesc `bson:"description" json:"description"`
|
||||
}
|
||||
|
||||
func (*Invitation) Collection() string {
|
||||
return mservice.Invitations
|
||||
}
|
||||
|
||||
type employeeDesc struct {
|
||||
Description Describable `bson:"description" json:"description"`
|
||||
AvatarURL *string `bson:"avatarUrl,omitempty" json:"avatarUrl,omitempty"`
|
||||
}
|
||||
|
||||
type organizationDesc struct {
|
||||
Description Describable `bson:"description" json:"description"`
|
||||
LogoURL *string `bson:"logoUrl,omitempty" json:"logoUrl,omitempty"`
|
||||
}
|
||||
|
||||
type invitationStorable struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
ExpiresAt time.Time `bson:"expiresAt" json:"expiresAt"`
|
||||
}
|
||||
|
||||
type PublicInvitation struct {
|
||||
Storable invitationStorable `bson:"storable" json:"storable"`
|
||||
Employee employeeDesc `bson:"employee" json:"employee"`
|
||||
Organization organizationDesc `bson:"organization" json:"organization"`
|
||||
Role Describable `bson:"role" json:"role"`
|
||||
Invitation invitationDesc `bson:"invitation" json:"invitation"`
|
||||
RequiresRegistration bool `bson:"registrationRequired" json:"registrationRequired"`
|
||||
}
|
||||
|
||||
func (pi *PublicInvitation) Collection() string {
|
||||
return mservice.Invitations
|
||||
}
|
||||
30
api/pkg/model/invoice.go
Normal file
30
api/pkg/model/invoice.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// InvoiceStatus represents the status of an invoice.
|
||||
type InvoiceStatus string
|
||||
|
||||
const (
|
||||
InvoiceStatusPending InvoiceStatus = "pending" // Invoice is created but not paid
|
||||
InvoiceStatusPaid InvoiceStatus = "paid" // Invoice has been fully paid
|
||||
InvoiceStatusCancelled InvoiceStatus = "cancelled" // Invoice has been cancelled
|
||||
)
|
||||
|
||||
type Invoice struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
Note string `bson:"note" json:"note"`
|
||||
Link *Link `bson:"link,omitempty" json:"link,omitempty"`
|
||||
OrganizationRef primitive.ObjectID `bson:"organizationRef" json:"organizationRef"`
|
||||
RecipientRef primitive.ObjectID `bson:"recipientRef" json:"recipientRef"`
|
||||
Amount Amount `bson:"amount" json:"amount"`
|
||||
Status InvoiceStatus `bson:"status" json:"status"` // Invoice status
|
||||
}
|
||||
|
||||
func (*Invoice) Collection() string {
|
||||
return mservice.Invoices
|
||||
}
|
||||
11
api/pkg/model/link.go
Normal file
11
api/pkg/model/link.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type Link struct {
|
||||
ObjectRef primitive.ObjectID `bson:"objectRef" json:"objectRef"`
|
||||
Type mservice.Type `bson:"type" json:"type"`
|
||||
}
|
||||
14
api/pkg/model/notification/notification.go
Normal file
14
api/pkg/model/notification/notification.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package model
|
||||
|
||||
type NotificationAction string
|
||||
|
||||
const (
|
||||
NACreated NotificationAction = "created"
|
||||
NAAssigned NotificationAction = "assigned"
|
||||
NAUpdated NotificationAction = "updated"
|
||||
NAPending NotificationAction = "pending"
|
||||
NADeleted NotificationAction = "deleted"
|
||||
NAArchived NotificationAction = "archived"
|
||||
NASent NotificationAction = "sent"
|
||||
NAPasswordReset NotificationAction = "password_reset"
|
||||
)
|
||||
91
api/pkg/model/notificationevent.go
Normal file
91
api/pkg/model/notificationevent.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
nm "github.com/tech/sendico/pkg/model/notification"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
)
|
||||
|
||||
type NotificationEvent interface {
|
||||
GetType() mservice.Type
|
||||
GetAction() nm.NotificationAction
|
||||
ToString() string
|
||||
StringType() string
|
||||
StringAction() string
|
||||
}
|
||||
|
||||
type NotificationEventImp struct {
|
||||
nType mservice.Type
|
||||
nAction nm.NotificationAction
|
||||
}
|
||||
|
||||
func (ne *NotificationEventImp) GetType() mservice.Type {
|
||||
return ne.nType
|
||||
}
|
||||
|
||||
func (ne *NotificationEventImp) GetAction() nm.NotificationAction {
|
||||
return ne.nAction
|
||||
}
|
||||
|
||||
const messageDelimiter string = "_"
|
||||
|
||||
func (ne *NotificationEventImp) Equals(other *NotificationEventImp) bool {
|
||||
return (other != nil) && (ne.nType == other.nType) && (ne.nAction == other.nAction)
|
||||
}
|
||||
|
||||
func (ne *NotificationEventImp) ToString() string {
|
||||
return ne.StringType() + messageDelimiter + ne.StringAction()
|
||||
}
|
||||
|
||||
func (ne *NotificationEventImp) StringType() string {
|
||||
return string(ne.nType)
|
||||
}
|
||||
|
||||
func (ne *NotificationEventImp) StringAction() string {
|
||||
return string(ne.nAction)
|
||||
}
|
||||
|
||||
func NewNotification(t mservice.Type, a nm.NotificationAction) NotificationEvent {
|
||||
return &NotificationEventImp{nType: t, nAction: a}
|
||||
}
|
||||
|
||||
func FromString(s string) (*NotificationEventImp, error) {
|
||||
parts := strings.Split(s, messageDelimiter)
|
||||
if len(parts) != 2 {
|
||||
return nil, merrors.Internal("invalid_notification_event_format")
|
||||
}
|
||||
|
||||
res := &NotificationEventImp{}
|
||||
var err error
|
||||
if res.nType, err = mservice.StringToSType(parts[0]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.nAction, err = StringToNotificationAction(parts[1]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func StringToNotificationAction(s string) (nm.NotificationAction, error) {
|
||||
switch nm.NotificationAction(s) {
|
||||
case nm.NACreated, nm.NAPending, nm.NAUpdated, nm.NADeleted, nm.NAAssigned, nm.NAPasswordReset:
|
||||
return nm.NotificationAction(s), nil
|
||||
default:
|
||||
return "", merrors.DataConflict("invalid Notification action: " + s)
|
||||
}
|
||||
}
|
||||
|
||||
func StringToNotificationEvent(eventType, eventAction string) (NotificationEvent, error) {
|
||||
et, err := mservice.StringToSType(eventType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ea, err := StringToNotificationAction(eventAction)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewNotification(et, ea), nil
|
||||
}
|
||||
9
api/pkg/model/nresult.go
Normal file
9
api/pkg/model/nresult.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package model
|
||||
|
||||
type NotificationResult struct {
|
||||
AmpliEvent
|
||||
Channel string
|
||||
Locale string
|
||||
TemplateID string
|
||||
Result OperationResult
|
||||
}
|
||||
20
api/pkg/model/object.go
Normal file
20
api/pkg/model/object.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type TypeBound struct {
|
||||
Type mservice.Type `bson:"type" json:"type"`
|
||||
}
|
||||
|
||||
type ObjectRefs struct {
|
||||
TypeBound `bson:"inline" json:"inline"`
|
||||
Refs []primitive.ObjectID `bson:"refs,omitempty" json:"refs,omitempty"`
|
||||
}
|
||||
|
||||
type ObjectRef struct {
|
||||
TypeBound `bson:"inline" json:"inline"`
|
||||
Ref primitive.ObjectID `bson:"ref,omitempty" json:"ref,omitempty"`
|
||||
}
|
||||
6
api/pkg/model/opresult.go
Normal file
6
api/pkg/model/opresult.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package model
|
||||
|
||||
type OperationResult struct {
|
||||
IsSuccessful bool
|
||||
Error string
|
||||
}
|
||||
44
api/pkg/model/organization.go
Normal file
44
api/pkg/model/organization.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type OrganizationBase struct {
|
||||
PermissionBound `bson:",inline" json:",inline"`
|
||||
Describable `bson:",inline" json:",inline"`
|
||||
TenantRef primitive.ObjectID `bson:"tenantRef" json:"tenantRef"`
|
||||
TimeZone string `bson:"timeZone" json:"timeZone"`
|
||||
LogoURL *string `bson:"logoUrl,omitempty" json:"logoUrl,omitempty"`
|
||||
}
|
||||
|
||||
func (*OrganizationBase) Collection() string {
|
||||
return mservice.Organizations
|
||||
}
|
||||
|
||||
type Organization struct {
|
||||
OrganizationBase `bson:",inline" json:",inline"`
|
||||
Members []primitive.ObjectID `bson:"members" json:"members"`
|
||||
}
|
||||
|
||||
type OrganizationBound interface {
|
||||
GetOrganizationRef() primitive.ObjectID
|
||||
SetOrganizationRef(organizationRef primitive.ObjectID)
|
||||
}
|
||||
|
||||
const (
|
||||
OrganizationRefField = "organizationRef"
|
||||
)
|
||||
|
||||
type OrganizationBoundBase struct {
|
||||
OrganizationRef primitive.ObjectID `bson:"organizationRef" json:"organizationRef"`
|
||||
}
|
||||
|
||||
func (a *OrganizationBoundBase) GetOrganizationRef() primitive.ObjectID {
|
||||
return a.OrganizationRef
|
||||
}
|
||||
|
||||
func (a *OrganizationBoundBase) SetOrganizationRef(organizationRef primitive.ObjectID) {
|
||||
a.OrganizationRef = organizationRef
|
||||
}
|
||||
32
api/pkg/model/pbinding.go
Normal file
32
api/pkg/model/pbinding.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type ScopeMode string
|
||||
|
||||
const (
|
||||
ScopeAll ScopeMode = "all" // apply to all of that type
|
||||
ScopeOnly ScopeMode = "only" // only listed IDs
|
||||
ScopeAllExcept ScopeMode = "all_except" // all minus listed IDs
|
||||
)
|
||||
|
||||
type TargetScope struct {
|
||||
ObjectRefs `bson:"target" json:"target"`
|
||||
Mode ScopeMode `bson:"mode" json:"mode"`
|
||||
}
|
||||
|
||||
type PropertyInstance struct {
|
||||
Global bool `bson:"global" json:"global"` // Property has single value for all property users
|
||||
Required bool `bson:"required" json:"required"` // Presence requirement (works for One and Many).
|
||||
UniqueAcrossEntities bool `bson:"uniqueAcrossEntities" json:"uniqueAcrossEntities"` // Uniqueness across ENTITIES (DB-level concern; enforce in assignments collection).
|
||||
PropertySchemaRef primitive.ObjectID `bson:"propertySchemaRef" json:"propertySchemaRef"`
|
||||
}
|
||||
|
||||
type PropertiesBinding struct {
|
||||
PermissionBound `bson:"inline" json:"inline"`
|
||||
Scope TargetScope `bson:"scope" json:"scope"`
|
||||
Bindings []PropertyInstance `bson:"bindings" json:"bindings"`
|
||||
ApplicableScopes []TargetScope `bson:"applicableScopes" json:"applicableScopes"`
|
||||
}
|
||||
33
api/pkg/model/permission.go
Normal file
33
api/pkg/model/permission.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type PermissionBoundStorable interface {
|
||||
storable.Storable
|
||||
OrganizationBound
|
||||
Archivable
|
||||
GetPermissionRef() primitive.ObjectID
|
||||
SetPermissionRef(permissionRef primitive.ObjectID)
|
||||
}
|
||||
|
||||
type PermissionBound struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
ArchivableBase `bson:",inline" json:",inline"`
|
||||
OrganizationBoundBase `bson:",inline" json:",inline"`
|
||||
PermissionRef primitive.ObjectID `bson:"permissionRef" json:"permissionRef"`
|
||||
}
|
||||
|
||||
func (b *PermissionBound) GetPermissionRef() primitive.ObjectID {
|
||||
return b.PermissionRef
|
||||
}
|
||||
|
||||
func (b *PermissionBound) GetOrganizationRef() primitive.ObjectID {
|
||||
return b.OrganizationRef
|
||||
}
|
||||
|
||||
func (b *PermissionBound) SetPermissionRef(permissionRef primitive.ObjectID) {
|
||||
b.PermissionRef = permissionRef
|
||||
}
|
||||
24
api/pkg/model/pfilter.go
Normal file
24
api/pkg/model/pfilter.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type ProjectFilterBase struct {
|
||||
ObjectsFilter `bson:",inline" json:",inline"`
|
||||
Archived *bool `bson:"isArchived,omitempty" json:"isArchived,omitempty"`
|
||||
AssigneeRefs []primitive.ObjectID `bson:"assigneeRefs,omitempty" json:"assigneeRefs,omitempty"`
|
||||
ReporterRefs []primitive.ObjectID `bson:"reporterRefs,omitempty" json:"reporterRefs,omitempty"`
|
||||
EmployeeRefs []primitive.ObjectID `bson:"employeeRefs,omitempty" json:"employeeRefs,omitempty"`
|
||||
}
|
||||
|
||||
type ProjectFilter struct {
|
||||
AccountBoundBase `bson:",inline" json:",inline"`
|
||||
Describable `bson:",inline" json:",inline"`
|
||||
ProjectFilterBase `bson:",inline" json:",inline"`
|
||||
}
|
||||
|
||||
func (*ProjectFilter) Collection() string {
|
||||
return mservice.FilterProjects
|
||||
}
|
||||
24
api/pkg/model/priority.go
Normal file
24
api/pkg/model/priority.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
)
|
||||
|
||||
type Priority struct {
|
||||
PermissionBound `bson:",inline" json:",inline"`
|
||||
Colorable `bson:",inline" json:",inline"`
|
||||
}
|
||||
|
||||
func (*Priority) Collection() string {
|
||||
return mservice.Priorities
|
||||
}
|
||||
|
||||
type PriorityGroup struct {
|
||||
PermissionBound `bson:",inline" json:",inline"`
|
||||
Describable `bson:",inline" json:",inline"`
|
||||
Priorities []IndexableRef `bson:"priorities" json:"priorities"`
|
||||
}
|
||||
|
||||
func (*PriorityGroup) Collection() string {
|
||||
return mservice.PriorityGroups
|
||||
}
|
||||
61
api/pkg/model/project.go
Normal file
61
api/pkg/model/project.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type ProjectState string
|
||||
|
||||
const (
|
||||
ProjectStateActive ProjectState = "active"
|
||||
ProjectStateHold ProjectState = "hold"
|
||||
ProjectStateBlocked ProjectState = "blocked"
|
||||
)
|
||||
|
||||
type ProjectBase struct {
|
||||
PermissionBound `bson:",inline" json:",inline"`
|
||||
Describable `bson:",inline" json:",inline"`
|
||||
Indexable `bson:",inline" json:",inline"`
|
||||
Taggable `bson:",inline" json:",inline"`
|
||||
LogoURL *string `bson:"logoUrl" json:"logoUrl"`
|
||||
Mnemonic string `bson:"mnemonic" json:"mnemonic"`
|
||||
State ProjectState `bson:"state" json:"state"`
|
||||
PriorityGroupRef primitive.ObjectID `bson:"priorityGroupRef" json:"priorityGroupRef"`
|
||||
StatusGroupRef primitive.ObjectID `bson:"statusGroupRef" json:"statusGroupRef"`
|
||||
}
|
||||
|
||||
func (*ProjectBase) Collection() string {
|
||||
return mservice.Projects
|
||||
}
|
||||
|
||||
type Project struct {
|
||||
ProjectBase `bson:",inline" json:",inline"`
|
||||
NextTaskNumber int `bson:"nextTaskNumber" json:"nextTaskNumber"`
|
||||
}
|
||||
|
||||
type ProjectOverallStats struct {
|
||||
TotalTasks int `json:"totalTasks" bson:"totalTasks"`
|
||||
OpenTasks int `json:"openTasks" bson:"openTasks"`
|
||||
OverDue int `json:"overDue" bson:"overDue"`
|
||||
NextDeadline *time.Time `json:"nextDeadline,omitempty" bson:"nextDeadline,omitempty"`
|
||||
}
|
||||
|
||||
// ProjectPersonallStatsD represents personal task statistics for a project.
|
||||
type ProjectPersonallStatsD struct {
|
||||
FreeTasks int `json:"freeTasks" bson:"freeTasks"`
|
||||
CompleteTasks int `json:"completeTasks" bson:"completeTasks"`
|
||||
MyTasks int `json:"myTasks" bson:"myTasks"`
|
||||
OverDue int `json:"overDue" bson:"overDue"`
|
||||
NextDeadline *time.Time `json:"nextDeadline,omitempty" bson:"nextDeadline,omitempty"`
|
||||
}
|
||||
|
||||
// ProjectPreview represents a preview of project information.
|
||||
type ProjectPreview struct {
|
||||
ProjectRef primitive.ObjectID `json:"projectRef" bson:"projectRef"`
|
||||
Team []primitive.ObjectID `json:"team" bson:"team"`
|
||||
Overall ProjectOverallStats `json:"overall" bson:"overall"`
|
||||
Personal ProjectPersonallStatsD `json:"personal" bson:"personal"`
|
||||
}
|
||||
671
api/pkg/model/property.go
Normal file
671
api/pkg/model/property.go
Normal file
@@ -0,0 +1,671 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
)
|
||||
|
||||
// ----------------------------
|
||||
// Core discriminant/type
|
||||
// ----------------------------
|
||||
|
||||
type PropertyType = string
|
||||
|
||||
const (
|
||||
PTDateTime PropertyType = "date_time"
|
||||
PTInteger PropertyType = "integer"
|
||||
PTFloat PropertyType = "float"
|
||||
PTMonetary PropertyType = "monetary"
|
||||
PTReference PropertyType = "reference"
|
||||
PTString PropertyType = "string"
|
||||
PTColor PropertyType = "color"
|
||||
PTObject PropertyType = "object"
|
||||
)
|
||||
|
||||
// Value keys for SettingsT maps
|
||||
const (
|
||||
VKString = "string"
|
||||
VKStrings = "strings"
|
||||
VKColor = "color"
|
||||
VKColors = "colors"
|
||||
VKInteger = "integer"
|
||||
VKIntegers = "integers"
|
||||
VKFloat = "float"
|
||||
VKFloats = "floats"
|
||||
VKDateTime = "date_time"
|
||||
VKDateTimes = "date_times"
|
||||
VKMonetary = "monetary"
|
||||
VKMonetaries = "monetaries"
|
||||
VKReference = "reference"
|
||||
VKReferences = "references"
|
||||
VKObject = "object"
|
||||
VKObjects = "objects"
|
||||
)
|
||||
|
||||
// Money struct field keys
|
||||
const (
|
||||
MKAmount = "amount"
|
||||
MKCurrency = "currency"
|
||||
)
|
||||
|
||||
// ----------------------------
|
||||
// Small value types (runtime values)
|
||||
// ----------------------------
|
||||
|
||||
// ----------------------------
|
||||
// Type-specific PROPS (schema/constraints)
|
||||
// ----------------------------
|
||||
|
||||
type IntegerProps struct {
|
||||
Default *int64 `bson:"default,omitempty" json:"default,omitempty"`
|
||||
Min *int64 `bson:"min,omitempty" json:"min,omitempty"`
|
||||
Max *int64 `bson:"max,omitempty" json:"max,omitempty"`
|
||||
Allowed []int64 `bson:"allowed,omitempty" json:"allowed,omitempty"`
|
||||
}
|
||||
|
||||
type FloatProps struct {
|
||||
Default *float64 `bson:"default,omitempty" json:"default,omitempty"`
|
||||
Min *float64 `bson:"min,omitempty" json:"min,omitempty"`
|
||||
Max *float64 `bson:"max,omitempty" json:"max,omitempty"`
|
||||
}
|
||||
|
||||
type StringProps struct {
|
||||
Default *string `bson:"default,omitempty" json:"default,omitempty"`
|
||||
Allowed []string `bson:"allowed,omitempty" json:"allowed,omitempty"`
|
||||
Pattern string `bson:"pattern" json:"pattern"` // Go RE2 syntax
|
||||
MinLen *int `bson:"minLen,omitempty" json:"minLen,omitempty"`
|
||||
MaxLen *int `bson:"maxLen,omitempty" json:"maxLen,omitempty"`
|
||||
}
|
||||
|
||||
type DateTimeProps struct {
|
||||
Default *time.Time `bson:"default,omitempty" json:"default,omitempty"` // store UTC
|
||||
Earliest *time.Time `bson:"earliest,omitempty" json:"earliest,omitempty"`
|
||||
Latest *time.Time `bson:"latest,omitempty" json:"latest,omitempty"`
|
||||
}
|
||||
|
||||
type ColorProps struct {
|
||||
AllowAlpha bool `bson:"allowAlpha,omitempty" json:"allowAlpha,omitempty"`
|
||||
AllowedPalette []string `bson:"allowedPalette,omitempty" json:"allowedPalette,omitempty"` // optional whitelist of hex colors
|
||||
Default string `bson:"default,omitempty" json:"default,omitempty"`
|
||||
}
|
||||
|
||||
type ObjectProps struct {
|
||||
Properties []PropertySchema `bson:"properties,omitempty" json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
// Currency policy for monetary props.
|
||||
type CurrencyMode string
|
||||
|
||||
const (
|
||||
CurrencyFixed CurrencyMode = "fixed" // force one currency (FixedCurrency)
|
||||
CurrencyOrg CurrencyMode = "org" // force org default currency at runtime
|
||||
CurrencyFree CurrencyMode = "free" // allow any (optionally restricted by AllowedCurrencies)
|
||||
)
|
||||
|
||||
type MonetaryProps struct {
|
||||
CurrencyMode CurrencyMode `bson:"currencyMode" json:"currencyMode"`
|
||||
FixedCurrency Currency `bson:"fixedCurrency" json:"fixedCurrency"` // required if fixed
|
||||
AllowedCurrencies []Currency `bson:"allowedCurrencies" json:"allowedCurrencies"` // for free mode
|
||||
|
||||
// Optional precision/rules; if nil, infer elsewhere by ISO minor units.
|
||||
Scale *int `bson:"scale,omitempty" json:"scale,omitempty"` // allowed decimal places
|
||||
Rounding *int `bson:"rounding,omitempty" json:"rounding,omitempty"` // app-specific; not enforced here
|
||||
|
||||
Default *Money `bson:"default,omitempty" json:"default,omitempty"`
|
||||
Min *Money `bson:"min,omitempty" json:"min,omitempty"`
|
||||
Max *Money `bson:"max,omitempty" json:"max,omitempty"`
|
||||
}
|
||||
|
||||
type ReferenceProps struct {
|
||||
Target mservice.Type `bson:"target" json:"target"` // e.g. "accounts"
|
||||
AllowedIDs []primitive.ObjectID `bson:"allowedIds,omitempty" json:"allowedIds,omitempty"` // optional whitelist
|
||||
Default *primitive.ObjectID `bson:"default,omitempty" json:"default,omitempty"` // optional default VALUE
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// UI hints (optional)
|
||||
// ----------------------------
|
||||
|
||||
type UIHints struct {
|
||||
Placeholder string `bson:"placeholder" json:"placeholder"`
|
||||
Unit string `bson:"unit" json:"unit"` // "kg", "cm", "€", etc.
|
||||
HiddenInList bool `bson:"hiddenInList" json:"hiddenInList"`
|
||||
Filterable bool `bson:"filterable" json:"filterable"`
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Multiplicity (generic, applies to any type)
|
||||
// ----------------------------
|
||||
|
||||
type Cardinality string
|
||||
|
||||
const (
|
||||
One Cardinality = "one" // single value
|
||||
Many Cardinality = "many" // array of values
|
||||
)
|
||||
|
||||
type Multiplicity struct {
|
||||
Mode Cardinality `bson:"mode" json:"mode"` // default "one"
|
||||
MinItems *int `bson:"minItems,omitempty" json:"minItems,omitempty"` // only when Mode=Many
|
||||
MaxItems *int `bson:"maxItems,omitempty" json:"maxItems,omitempty"` // only when Mode=Many
|
||||
// Distinct within one entity's list value (meaningful for Mode=Many).
|
||||
Distinct bool `bson:"distinct" json:"distinct"`
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Property envelope
|
||||
// ----------------------------
|
||||
|
||||
type PropertySchema struct {
|
||||
PermissionBound `bson:",inline" json:",inline"`
|
||||
Describable `bson:",inline" json:",inline"`
|
||||
|
||||
// customer permission refernece
|
||||
ValuePermissionRef *primitive.ObjectID `bson:"valuePermissionRef,omitempty" json:"valuePermissionRef,omitempty"`
|
||||
|
||||
// Stable machine key; unique within (organizatoinRef, type, key)
|
||||
Key string `bson:"key" json:"key"`
|
||||
Type PropertyType `bson:"type" json:"type"`
|
||||
|
||||
// Lifecycle/UX
|
||||
System bool `bson:"system" json:"system"`
|
||||
UI *UIHints `bson:"ui,omitempty" json:"ui,omitempty"`
|
||||
|
||||
// Multiplicity controls (cross-type).
|
||||
Multiplicity Multiplicity `bson:"multiplicity" json:"multiplicity"`
|
||||
|
||||
// Discriminated payload; a BSON subdocument shaped per Type.
|
||||
Props any `bson:"props" json:"props"`
|
||||
}
|
||||
|
||||
func (*PropertySchema) Collection() string { return mservice.PropertySchemas }
|
||||
|
||||
// ----------------------------
|
||||
// Typed accessors for Props
|
||||
// ----------------------------
|
||||
|
||||
func invalidType(expected, actual PropertyType) error {
|
||||
return merrors.InvalidDataType(fmt.Sprintf("expected type is %s while actual type is %s", expected, actual))
|
||||
}
|
||||
|
||||
// asTypedProps is a generic function that handles type checking and casting for all property types
|
||||
func asTypedProps[T any](p *PropertySchema, expectedType PropertyType) (T, error) {
|
||||
var out T
|
||||
if p.Type != expectedType {
|
||||
return out, invalidType(expectedType, p.Type)
|
||||
}
|
||||
// Props is stored directly as the correct type, so we can cast it
|
||||
if props, ok := p.Props.(T); ok {
|
||||
return props, nil
|
||||
}
|
||||
return out, merrors.InvalidArgument("invalid props type")
|
||||
}
|
||||
|
||||
// Type-specific accessor functions using the generic template
|
||||
func (p *PropertySchema) AsInteger() (IntegerProps, error) {
|
||||
return asTypedProps[IntegerProps](p, PTInteger)
|
||||
}
|
||||
|
||||
func (p *PropertySchema) AsFloat() (FloatProps, error) {
|
||||
return asTypedProps[FloatProps](p, PTFloat)
|
||||
}
|
||||
|
||||
func (p *PropertySchema) AsString() (StringProps, error) {
|
||||
return asTypedProps[StringProps](p, PTString)
|
||||
}
|
||||
|
||||
func (p *PropertySchema) AsDateTime() (DateTimeProps, error) {
|
||||
return asTypedProps[DateTimeProps](p, PTDateTime)
|
||||
}
|
||||
|
||||
func (p *PropertySchema) AsMonetary() (MonetaryProps, error) {
|
||||
return asTypedProps[MonetaryProps](p, PTMonetary)
|
||||
}
|
||||
|
||||
func (p *PropertySchema) AsReference() (ReferenceProps, error) {
|
||||
return asTypedProps[ReferenceProps](p, PTReference)
|
||||
}
|
||||
|
||||
func (p *PropertySchema) AsColor() (ColorProps, error) {
|
||||
return asTypedProps[ColorProps](p, PTColor)
|
||||
}
|
||||
|
||||
func (p *PropertySchema) AsObject() (ObjectProps, error) {
|
||||
return asTypedProps[ObjectProps](p, PTObject)
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Validation helpers (generic)
|
||||
// ----------------------------
|
||||
|
||||
func validateMultiplicity(count int, required bool, m Multiplicity) error {
|
||||
mode := m.Mode
|
||||
if mode == "" {
|
||||
mode = One
|
||||
}
|
||||
switch mode {
|
||||
case One:
|
||||
if count > 1 {
|
||||
return merrors.DataConflict("multiple values not allowed")
|
||||
}
|
||||
if required && count == 0 {
|
||||
return merrors.DataConflict("value required")
|
||||
}
|
||||
case Many:
|
||||
min := 0
|
||||
if m.MinItems != nil {
|
||||
min = *m.MinItems
|
||||
} else if required {
|
||||
min = 1
|
||||
}
|
||||
if count < min {
|
||||
return merrors.DataConflict(fmt.Sprintf("minimum %d items", min))
|
||||
}
|
||||
if m.MaxItems != nil && count > *m.MaxItems {
|
||||
return merrors.DataConflict(fmt.Sprintf("maximum %d items", *m.MaxItems))
|
||||
}
|
||||
default:
|
||||
return merrors.InvalidArgument(fmt.Sprintf("unknown cardinality: %q", mode))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureDistinct[T comparable](vals []T, distinct bool) error {
|
||||
if !distinct || len(vals) < 2 {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[T]struct{}, len(vals))
|
||||
for _, v := range vals {
|
||||
if _, ok := seen[v]; ok {
|
||||
return merrors.DataConflict("duplicate items not allowed")
|
||||
}
|
||||
seen[v] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureDistinctByKey[T any, K comparable](vals []T, key func(T) K, distinct bool) error {
|
||||
if !distinct || len(vals) < 2 {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[K]struct{}, len(vals))
|
||||
for _, v := range vals {
|
||||
k := key(v)
|
||||
if _, ok := seen[k]; ok {
|
||||
return merrors.DataConflict("duplicate items not allowed")
|
||||
}
|
||||
seen[k] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Type validators
|
||||
// ----------------------------
|
||||
|
||||
func (p PropertySchema) ValidateStrings(vals []string) error {
|
||||
if p.Type != PTString {
|
||||
return invalidType(PTString, p.Type)
|
||||
}
|
||||
if err := validateMultiplicity(len(vals), false, p.Multiplicity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureDistinct(vals, p.Multiplicity.Distinct); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
props, err := p.AsString()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var re *regexp.Regexp
|
||||
if props.Pattern != "" {
|
||||
rx, rxErr := regexp.Compile(props.Pattern)
|
||||
if rxErr != nil {
|
||||
return merrors.InvalidArgument(fmt.Sprintf("invalid pattern: %v", rxErr))
|
||||
}
|
||||
re = rx
|
||||
}
|
||||
|
||||
allow := map[string]struct{}{}
|
||||
if len(props.Allowed) > 0 {
|
||||
for _, a := range props.Allowed {
|
||||
allow[a] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range vals {
|
||||
if len(allow) > 0 {
|
||||
if _, ok := allow[v]; !ok {
|
||||
return merrors.DataConflict(fmt.Sprintf("value %q not allowed", v))
|
||||
}
|
||||
}
|
||||
if props.MinLen != nil && len(v) < *props.MinLen {
|
||||
return merrors.DataConflict(fmt.Sprintf("value too short (min %d)", *props.MinLen))
|
||||
}
|
||||
if props.MaxLen != nil && len(v) > *props.MaxLen {
|
||||
return merrors.DataConflict(fmt.Sprintf("value too long (max %d)", *props.MaxLen))
|
||||
}
|
||||
if re != nil && !re.MatchString(v) {
|
||||
return merrors.DataConflict(fmt.Sprintf("value %q does not match pattern", v))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p PropertySchema) ValidateColors(vals []string) error {
|
||||
if p.Type != PTColor {
|
||||
return invalidType(PTColor, p.Type)
|
||||
}
|
||||
if err := validateMultiplicity(len(vals), false, p.Multiplicity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureDistinct(vals, p.Multiplicity.Distinct); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := p.AsColor()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// For now, we can use the same validation as strings
|
||||
// In the future, we might want to add color-specific validation
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p PropertySchema) ValidateIntegers(vals []int64) error {
|
||||
if p.Type != PTInteger {
|
||||
return invalidType(PTInteger, p.Type)
|
||||
}
|
||||
if err := validateMultiplicity(len(vals), false, p.Multiplicity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureDistinct(vals, p.Multiplicity.Distinct); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
props, err := p.AsInteger()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
allow := map[int64]struct{}{}
|
||||
if len(props.Allowed) > 0 {
|
||||
for _, a := range props.Allowed {
|
||||
allow[a] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range vals {
|
||||
if len(allow) > 0 {
|
||||
if _, ok := allow[v]; !ok {
|
||||
return merrors.DataConflict(fmt.Sprintf("value %d not allowed", v))
|
||||
}
|
||||
}
|
||||
if props.Min != nil && v < *props.Min {
|
||||
return merrors.DataConflict(fmt.Sprintf("value %d below min %d", v, *props.Min))
|
||||
}
|
||||
if props.Max != nil && v > *props.Max {
|
||||
return merrors.DataConflict(fmt.Sprintf("value %d above max %d", v, *props.Max))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p PropertySchema) ValidateFloats(vals []float64) error {
|
||||
if p.Type != PTFloat {
|
||||
return invalidType(PTFloat, p.Type)
|
||||
}
|
||||
if err := validateMultiplicity(len(vals), false, p.Multiplicity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureDistinct(vals, p.Multiplicity.Distinct); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
props, err := p.AsFloat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, v := range vals {
|
||||
if props.Min != nil && v < *props.Min {
|
||||
return merrors.DataConflict(fmt.Sprintf("value %g below min %g", v, *props.Min))
|
||||
}
|
||||
if props.Max != nil && v > *props.Max {
|
||||
return merrors.DataConflict(fmt.Sprintf("value %g above max %g", v, *props.Max))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p PropertySchema) ValidateDateTimes(vals []time.Time) error {
|
||||
if p.Type != PTDateTime {
|
||||
return invalidType(PTDateTime, p.Type)
|
||||
}
|
||||
if err := validateMultiplicity(len(vals), false, p.Multiplicity); err != nil {
|
||||
return err
|
||||
}
|
||||
// Distinct datetimes rarely matter; honor it if requested.
|
||||
if err := ensureDistinctByKey(vals, func(t time.Time) int64 { return t.UTC().UnixNano() }, p.Multiplicity.Distinct); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
props, err := p.AsDateTime()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, v := range vals {
|
||||
vu := v.UTC()
|
||||
if props.Earliest != nil && vu.Before(props.Earliest.UTC()) {
|
||||
return merrors.DataConflict("datetime before earliest")
|
||||
}
|
||||
if props.Latest != nil && vu.After(props.Latest.UTC()) {
|
||||
return merrors.DataConflict("datetime after latest")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Monetary validation (handles currency policy + Min/Max + optional scale)
|
||||
func (p PropertySchema) ValidateMonetaries(vals []Money, orgCurrency Currency) error {
|
||||
if p.Type != PTMonetary {
|
||||
return invalidType(PTMonetary, p.Type)
|
||||
}
|
||||
if err := validateMultiplicity(len(vals), false, p.Multiplicity); err != nil {
|
||||
return err
|
||||
}
|
||||
// Distinct by (currency, amount)
|
||||
if err := ensureDistinctByKey(vals, func(m Money) string { return string(m.Currency) + "|" + m.Amount.String() }, p.Multiplicity.Distinct); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
props, err := p.AsMonetary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
allowedCur := map[Currency]struct{}{}
|
||||
if len(props.AllowedCurrencies) > 0 {
|
||||
for _, c := range props.AllowedCurrencies {
|
||||
allowedCur[c] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range vals {
|
||||
// Currency policy
|
||||
switch props.CurrencyMode {
|
||||
case CurrencyFixed:
|
||||
if props.FixedCurrency == "" {
|
||||
return merrors.InvalidArgument("fixed currency is not configured")
|
||||
}
|
||||
if v.Currency != props.FixedCurrency {
|
||||
return merrors.DataConflict(fmt.Sprintf("currency must be %s", props.FixedCurrency))
|
||||
}
|
||||
case CurrencyOrg:
|
||||
if orgCurrency == "" {
|
||||
return merrors.InvalidArgument("org currency not provided")
|
||||
}
|
||||
if v.Currency != Currency(orgCurrency) {
|
||||
return merrors.DataConflict(fmt.Sprintf("currency must be %s", orgCurrency))
|
||||
}
|
||||
case CurrencyFree, "":
|
||||
if len(allowedCur) > 0 {
|
||||
if _, ok := allowedCur[v.Currency]; !ok {
|
||||
return merrors.DataConflict(fmt.Sprintf("currency %s not allowed", v.Currency))
|
||||
}
|
||||
}
|
||||
default:
|
||||
return merrors.InvalidArgument(fmt.Sprintf("unknown currency mode: %s", props.CurrencyMode))
|
||||
}
|
||||
|
||||
// Scale check (if configured)
|
||||
if props.Scale != nil {
|
||||
ok, frac := decimal128WithinScale(v.Amount, *props.Scale)
|
||||
if !ok {
|
||||
return merrors.DataConflict(fmt.Sprintf("too many decimal places: got %d, max %d", frac, *props.Scale))
|
||||
}
|
||||
}
|
||||
|
||||
// Min/Max (apply only if currencies match)
|
||||
if props.Min != nil && props.Min.Currency == v.Currency {
|
||||
cmp, cmpErr := compareDecimal128(v.Amount, props.Min.Amount)
|
||||
if cmpErr == nil && cmp < 0 {
|
||||
return merrors.DataConflict("amount below min")
|
||||
}
|
||||
}
|
||||
if props.Max != nil && props.Max.Currency == v.Currency {
|
||||
cmp, cmpErr := compareDecimal128(v.Amount, props.Max.Amount)
|
||||
if cmpErr == nil && cmp > 0 {
|
||||
return merrors.DataConflict("amount above max")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// References: existence check is injected.
|
||||
type ExistFn func(resource mservice.Type, id primitive.ObjectID, filter bson.M) (bool, error)
|
||||
|
||||
func (p PropertySchema) ValidateReferences(vals []primitive.ObjectID, exist ExistFn) error {
|
||||
if p.Type != PTReference {
|
||||
return invalidType(PTReference, p.Type)
|
||||
}
|
||||
if err := validateMultiplicity(len(vals), false, p.Multiplicity); err != nil {
|
||||
return err
|
||||
}
|
||||
props, err := p.AsReference()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Distinct by referenced ID (and resource)
|
||||
if err := ensureDistinctByKey(vals, func(r primitive.ObjectID) string { return props.Target + ":" + r.Hex() }, p.Multiplicity.Distinct); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
allowed := map[primitive.ObjectID]struct{}{}
|
||||
if len(props.AllowedIDs) > 0 {
|
||||
for _, id := range props.AllowedIDs {
|
||||
allowed[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range vals {
|
||||
if len(allowed) > 0 {
|
||||
if _, ok := allowed[v]; !ok {
|
||||
return merrors.DataConflict(fmt.Sprintf("id %s not allowed", v.Hex()))
|
||||
}
|
||||
}
|
||||
if exist != nil {
|
||||
ok, exErr := exist(props.Target, v, bson.M{})
|
||||
if exErr != nil {
|
||||
return exErr
|
||||
}
|
||||
if !ok {
|
||||
return merrors.DataConflict("referenced document not found or disallowed")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Decimal128 utilities
|
||||
// ----------------------------
|
||||
|
||||
// compareDecimal128 returns -1 if a < b, 0 if a == b, 1 if a > b.
|
||||
func compareDecimal128(a, b primitive.Decimal128) (int, error) {
|
||||
as := a.String()
|
||||
bs := b.String()
|
||||
|
||||
af, _, err := big.ParseFloat(as, 10, 128, big.ToNearestEven)
|
||||
if err != nil {
|
||||
return 0, merrors.InvalidArgument(err.Error())
|
||||
}
|
||||
bf, _, err := big.ParseFloat(bs, 10, 128, big.ToNearestEven)
|
||||
if err != nil {
|
||||
return 0, merrors.InvalidArgument(err.Error())
|
||||
}
|
||||
return af.Cmp(bf), nil
|
||||
}
|
||||
|
||||
// decimal128WithinScale checks if the number of fractional digits is <= scale.
|
||||
func decimal128WithinScale(d primitive.Decimal128, scale int) (ok bool, fracDigits int) {
|
||||
// Normalize via big.Float to handle exponents; then trim trailing zeros.
|
||||
s := d.String()
|
||||
f, _, err := big.ParseFloat(s, 10, 128, big.ToNearestEven)
|
||||
if err != nil {
|
||||
fd := countFractionDigits(s)
|
||||
return fd <= scale, fd
|
||||
}
|
||||
fixed := f.Text('f', 40) // enough precision
|
||||
fixed = trimTrailingZeros(fixed)
|
||||
fd := countFractionDigits(fixed)
|
||||
return fd <= scale, fd
|
||||
}
|
||||
|
||||
func countFractionDigits(s string) int {
|
||||
dot := -1
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == '.' {
|
||||
dot = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if dot < 0 {
|
||||
return 0
|
||||
}
|
||||
return len(s) - dot - 1
|
||||
}
|
||||
|
||||
func trimTrailingZeros(s string) string {
|
||||
dot := -1
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == '.' {
|
||||
dot = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if dot < 0 {
|
||||
return s
|
||||
}
|
||||
j := len(s) - 1
|
||||
for j > dot && s[j] == '0' {
|
||||
j--
|
||||
}
|
||||
if j == dot {
|
||||
return s[:dot]
|
||||
}
|
||||
return s[:j+1]
|
||||
}
|
||||
23
api/pkg/model/reaction.go
Normal file
23
api/pkg/model/reaction.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type ReactionType string
|
||||
|
||||
const (
|
||||
ThumbsUp ReactionType = "thumbs_up"
|
||||
ThumbsDown ReactionType = "thumbs_down"
|
||||
Heart ReactionType = "heart"
|
||||
Laugh ReactionType = "laugh"
|
||||
Question ReactionType = "question"
|
||||
Exclamation ReactionType = "exclamation"
|
||||
)
|
||||
|
||||
type Reaction struct {
|
||||
PermissionBound `bson:",inline" json:",inline"`
|
||||
Type ReactionType `json:"type"`
|
||||
AuthorRef primitive.ObjectID `json:"authorRef"`
|
||||
CommentRef primitive.ObjectID `json:"commentRef"`
|
||||
}
|
||||
26
api/pkg/model/refresh.go
Normal file
26
api/pkg/model/refresh.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
)
|
||||
|
||||
type ClientRefreshToken struct {
|
||||
SessionIdentifier `bson:",inline" json:",inline"`
|
||||
RefreshToken string `bson:"token" json:"token"`
|
||||
}
|
||||
|
||||
type RefreshToken struct {
|
||||
AccountBoundBase `bson:",inline" json:",inline"`
|
||||
ClientRefreshToken `bson:",inline" json:",inline"`
|
||||
ExpiresAt time.Time `bson:"expiresAt"`
|
||||
IsRevoked bool `bson:"isRevoked"`
|
||||
LastUsedAt time.Time `bson:"lastUsedAt,omitempty"`
|
||||
UserAgent string `bson:"userAgent"`
|
||||
IPAddress string `bson:"ipAddress"`
|
||||
}
|
||||
|
||||
func (*RefreshToken) Collection() string {
|
||||
return mservice.RefreshTokens
|
||||
}
|
||||
6
api/pkg/model/sessionid.go
Normal file
6
api/pkg/model/sessionid.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package model
|
||||
|
||||
type SessionIdentifier struct {
|
||||
ClientID string `bson:"clientId" json:"clientId"`
|
||||
DeviceID string `bson:"deviceId" json:"deviceId"`
|
||||
}
|
||||
26
api/pkg/model/status.go
Normal file
26
api/pkg/model/status.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
)
|
||||
|
||||
type Status struct {
|
||||
PermissionBound `bson:",inline" json:",inline"`
|
||||
Colorable `bson:",inline" json:",inline"`
|
||||
Icon string `bson:"icon" json:"icon"`
|
||||
IsFinal bool `bson:"isFinal" json:"isFinal"`
|
||||
}
|
||||
|
||||
func (*Status) Collection() string {
|
||||
return mservice.Statuses
|
||||
}
|
||||
|
||||
type StatusGroup struct {
|
||||
PermissionBound `bson:",inline" json:",inline"`
|
||||
Describable `bson:",inline" json:",inline"`
|
||||
Statuses []IndexableRef `bson:"statuses" json:"statuses"`
|
||||
}
|
||||
|
||||
func (*StatusGroup) Collection() string {
|
||||
return mservice.StatusGroups
|
||||
}
|
||||
20
api/pkg/model/step.go
Normal file
20
api/pkg/model/step.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type Step struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
ArchivableBase `bson:",inline" json:",inline"`
|
||||
Colorable `bson:",inline" json:",inline"`
|
||||
StatusRef primitive.ObjectID `bson:"statusRef" json:"statusRef"` // Reference to dynamic status
|
||||
NextSteps []primitive.ObjectID `bson:"nextSteps" json:"nextSteps"` // Allowed transitions
|
||||
Automations []primitive.ObjectID `bson:"automations" json:"automations"` // Automatically executed steps
|
||||
}
|
||||
|
||||
func (*Step) Collection() string {
|
||||
return mservice.Steps
|
||||
}
|
||||
23
api/pkg/model/tag.go
Normal file
23
api/pkg/model/tag.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
const TagRefsField = "tagRefs"
|
||||
|
||||
type Tag struct {
|
||||
PermissionBound `bson:",inline" json:",inline"`
|
||||
Describable `bson:",inline" json:",inline"`
|
||||
Colorable `bson:",inline" json:",inline"`
|
||||
TypeRefs *[]mservice.Type `bson:"typeRefs,omitempty" json:"typeRefs,omitempty"`
|
||||
}
|
||||
|
||||
func (*Tag) Collection() string {
|
||||
return mservice.Tags
|
||||
}
|
||||
|
||||
type Taggable struct {
|
||||
TagRefs []primitive.ObjectID `bson:"tagRefs,omitempty" json:"tagRefs,omitempty"`
|
||||
}
|
||||
26
api/pkg/model/task.go
Normal file
26
api/pkg/model/task.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type Task struct {
|
||||
PermissionBound `bson:",inline" json:",inline"`
|
||||
Describable `bson:",inline" json:",inline"`
|
||||
Indexable `bson:",inline" json:",inline"`
|
||||
Taggable `bson:",inline" json:",inline"`
|
||||
StatusRef primitive.ObjectID `bson:"statusRef" json:"statusRef"` // Reference to the current Step
|
||||
ReporterRef primitive.ObjectID `bson:"reporterRef" json:"reporterRef"` // Reference to the task reporter
|
||||
AssigneeRef *primitive.ObjectID `bson:"assigneeRef,omitempty" json:"assigneeRef,omitempty"` // Reference to the user assigned
|
||||
ProjectRef primitive.ObjectID `bson:"projectRef" json:"projectRef"` // Reference to the project
|
||||
PriorityRef primitive.ObjectID `bson:"priorityRef" json:"priorityRef"` // Reference to dynamic priority
|
||||
DueDate *time.Time `bson:"dueDate" json:"dueDate"`
|
||||
Number int `bson:"number" json:"number"`
|
||||
}
|
||||
|
||||
func (*Task) Collection() string {
|
||||
return mservice.Tasks
|
||||
}
|
||||
19
api/pkg/model/team.go
Normal file
19
api/pkg/model/team.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type Team struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
Describable `bson:",inline" json:",inline"`
|
||||
OrganizationRef primitive.ObjectID `bson:"organizationRef" json:"organizationRef"`
|
||||
MemberRefs []primitive.ObjectID `bson:"memberRefs" json:"memberRefs"`
|
||||
SubTeamsRefs []primitive.ObjectID `bson:"subteamsRefs" json:"subteamsRefs"`
|
||||
}
|
||||
|
||||
func (*Team) Collection() string {
|
||||
return mservice.Teams
|
||||
}
|
||||
15
api/pkg/model/tenant.go
Normal file
15
api/pkg/model/tenant.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
)
|
||||
|
||||
type Tenant struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
Describable `bson:",inline" json:",inline"`
|
||||
}
|
||||
|
||||
func (*Tenant) Collection() string {
|
||||
return mservice.Tenants
|
||||
}
|
||||
30
api/pkg/model/userdata.go
Normal file
30
api/pkg/model/userdata.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package model
|
||||
|
||||
type UserDataBase struct {
|
||||
Login string `bson:"login" json:"login"`
|
||||
Locale string `bson:"locale" json:"locale"`
|
||||
}
|
||||
|
||||
type LoginData struct {
|
||||
UserDataBase `bson:",inline" json:",inline"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type AccountData struct {
|
||||
LoginData `bson:",inline" json:",inline"`
|
||||
Name string `bson:"name" json:"name"`
|
||||
}
|
||||
|
||||
func (ad *AccountData) ToAccount() *Account {
|
||||
return &Account{
|
||||
AccountPublic: AccountPublic{
|
||||
AccountBase: AccountBase{
|
||||
Describable: Describable{
|
||||
Name: ad.Name,
|
||||
},
|
||||
},
|
||||
UserDataBase: ad.UserDataBase,
|
||||
},
|
||||
Password: ad.Password,
|
||||
}
|
||||
}
|
||||
751
api/pkg/model/value.go
Normal file
751
api/pkg/model/value.go
Normal file
@@ -0,0 +1,751 @@
|
||||
// file: model/value.go
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
)
|
||||
|
||||
// ----------------------------
|
||||
// Assignment model (domain)
|
||||
// ----------------------------
|
||||
type Value struct {
|
||||
PermissionBound `bson:",inline" json:",inline"`
|
||||
|
||||
Target ObjectRef `bson:"target" json:"target"`
|
||||
Type PropertyType `bson:"type" json:"type"`
|
||||
Cardinality Cardinality `bson:"cardinality" json:"cardinality"`
|
||||
|
||||
PropertySchemaRef primitive.ObjectID `bson:"propertySchemaRef" json:"propertySchemaRef"`
|
||||
|
||||
// Small typed shape via keys like: "string"/"strings", "integer"/"integers", etc.
|
||||
Values SettingsT `bson:"data" json:"data" yaml:"data"`
|
||||
}
|
||||
|
||||
type Money struct {
|
||||
Amount primitive.Decimal128 `bson:"amount" json:"amount"`
|
||||
Currency Currency `bson:"currency" json:"currency"`
|
||||
}
|
||||
|
||||
type Object = map[string]Value
|
||||
|
||||
// ----------------------------
|
||||
// SINGLE getters
|
||||
// ----------------------------
|
||||
|
||||
func (v *Value) AsString() (string, error) {
|
||||
if v.Type != PTString {
|
||||
return "", invalidType(PTString, v.Type)
|
||||
}
|
||||
if v.Cardinality != One {
|
||||
return "", merrors.InvalidArgument("invalid cardinality: expected one")
|
||||
}
|
||||
type payload struct {
|
||||
Value string `mapstructure:"string" bson:"string" json:"string" yaml:"string"`
|
||||
}
|
||||
var p payload
|
||||
if err := mapstructure.Decode(v.Values, &p); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return p.Value, nil
|
||||
}
|
||||
|
||||
func (v *Value) AsColor() (string, error) {
|
||||
if v.Type != PTColor {
|
||||
return "", invalidType(PTColor, v.Type)
|
||||
}
|
||||
if v.Cardinality != One {
|
||||
return "", merrors.InvalidArgument("invalid cardinality: expected one")
|
||||
}
|
||||
type payload struct {
|
||||
Value string `mapstructure:"color" bson:"color" json:"color" yaml:"color"`
|
||||
}
|
||||
var p payload
|
||||
if err := mapstructure.Decode(v.Values, &p); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return p.Value, nil
|
||||
}
|
||||
|
||||
func (v *Value) AsInteger() (int64, error) {
|
||||
if v.Type != PTInteger {
|
||||
return 0, invalidType(PTInteger, v.Type)
|
||||
}
|
||||
if v.Cardinality != One {
|
||||
return 0, merrors.InvalidArgument("invalid cardinality: expected one")
|
||||
}
|
||||
type payload struct {
|
||||
Value int64 `mapstructure:"integer" bson:"integer" json:"integer" yaml:"integer"`
|
||||
}
|
||||
var p payload
|
||||
if err := mapstructure.Decode(v.Values, &p); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return p.Value, nil
|
||||
}
|
||||
|
||||
func (v *Value) AsFloat() (float64, error) {
|
||||
if v.Type != PTFloat {
|
||||
return 0, invalidType(PTFloat, v.Type)
|
||||
}
|
||||
if v.Cardinality != One {
|
||||
return 0, merrors.InvalidArgument("invalid cardinality: expected one")
|
||||
}
|
||||
type payload struct {
|
||||
Value float64 `mapstructure:"float" bson:"float" json:"float" yaml:"float"`
|
||||
}
|
||||
var p payload
|
||||
if err := mapstructure.Decode(v.Values, &p); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return p.Value, nil
|
||||
}
|
||||
|
||||
func (v *Value) AsDateTime() (time.Time, error) {
|
||||
if v.Type != PTDateTime {
|
||||
return time.Time{}, invalidType(PTDateTime, v.Type)
|
||||
}
|
||||
if v.Cardinality != One {
|
||||
return time.Time{}, merrors.InvalidArgument("invalid cardinality: expected one")
|
||||
}
|
||||
type payload struct {
|
||||
Value time.Time `mapstructure:"date_time" bson:"date_time" json:"date_time" yaml:"date_time"`
|
||||
}
|
||||
var p payload
|
||||
if err := mapstructure.Decode(v.Values, &p); err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return p.Value, nil
|
||||
}
|
||||
|
||||
func (v *Value) AsMonetary() (Money, error) {
|
||||
if v.Type != PTMonetary {
|
||||
return Money{}, invalidType(PTMonetary, v.Type)
|
||||
}
|
||||
if v.Cardinality != One {
|
||||
return Money{}, merrors.InvalidArgument("invalid cardinality: expected one")
|
||||
}
|
||||
type payload struct {
|
||||
Value Money `mapstructure:"monetary" bson:"monetary" json:"monetary" yaml:"monetary"`
|
||||
}
|
||||
var p payload
|
||||
if err := mapstructure.Decode(v.Values, &p); err != nil {
|
||||
return Money{}, err
|
||||
}
|
||||
return p.Value, nil
|
||||
}
|
||||
|
||||
func (v *Value) AsReference() (primitive.ObjectID, error) {
|
||||
if v.Type != PTReference {
|
||||
return primitive.NilObjectID, invalidType(PTReference, v.Type)
|
||||
}
|
||||
if v.Cardinality != One {
|
||||
return primitive.NilObjectID, merrors.InvalidArgument("invalid cardinality: expected one")
|
||||
}
|
||||
type payload struct {
|
||||
Value primitive.ObjectID `mapstructure:"reference" bson:"reference" json:"reference" yaml:"reference"`
|
||||
}
|
||||
var p payload
|
||||
if err := mapstructure.Decode(v.Values, &p); err != nil {
|
||||
return primitive.NilObjectID, err
|
||||
}
|
||||
return p.Value, nil
|
||||
}
|
||||
|
||||
func (v *Value) AsObject() (Object, error) {
|
||||
if v.Type != PTObject {
|
||||
return nil, invalidType(PTObject, v.Type)
|
||||
}
|
||||
if v.Cardinality != One {
|
||||
return nil, merrors.InvalidArgument("invalid cardinality: expected one")
|
||||
}
|
||||
type payload struct {
|
||||
Value Object `mapstructure:"object" bson:"object" json:"object" yaml:"object"`
|
||||
}
|
||||
var p payload
|
||||
if err := mapstructure.Decode(v.Values, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.Value, nil
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// ARRAY getters
|
||||
// ----------------------------
|
||||
|
||||
func (v *Value) AsStrings() ([]string, error) {
|
||||
if v.Type != PTString {
|
||||
return nil, invalidType(PTString, v.Type)
|
||||
}
|
||||
if v.Cardinality != Many {
|
||||
return nil, merrors.InvalidArgument("invalid cardinality: expected many")
|
||||
}
|
||||
type payload struct {
|
||||
Values []string `mapstructure:"strings" bson:"strings" json:"strings" yaml:"strings"`
|
||||
}
|
||||
var p payload
|
||||
if err := mapstructure.Decode(v.Values, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.Values, nil
|
||||
}
|
||||
|
||||
func (v *Value) AsColors() ([]string, error) {
|
||||
if v.Type != PTColor {
|
||||
return nil, invalidType(PTColor, v.Type)
|
||||
}
|
||||
if v.Cardinality != Many {
|
||||
return nil, merrors.InvalidArgument("invalid cardinality: expected many")
|
||||
}
|
||||
type payload struct {
|
||||
Values []string `mapstructure:"colors" bson:"colors" json:"colors" yaml:"colors"`
|
||||
}
|
||||
var p payload
|
||||
if err := mapstructure.Decode(v.Values, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.Values, nil
|
||||
}
|
||||
|
||||
func (v *Value) AsIntegers() ([]int64, error) {
|
||||
if v.Type != PTInteger {
|
||||
return nil, invalidType(PTInteger, v.Type)
|
||||
}
|
||||
if v.Cardinality != Many {
|
||||
return nil, merrors.InvalidArgument("invalid cardinality: expected many")
|
||||
}
|
||||
type payload struct {
|
||||
Values []int64 `mapstructure:"integers" bson:"integers" json:"integers" yaml:"integers"`
|
||||
}
|
||||
var p payload
|
||||
if err := mapstructure.Decode(v.Values, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.Values, nil
|
||||
}
|
||||
|
||||
func (v *Value) AsFloats() ([]float64, error) {
|
||||
if v.Type != PTFloat {
|
||||
return nil, invalidType(PTFloat, v.Type)
|
||||
}
|
||||
if v.Cardinality != Many {
|
||||
return nil, merrors.InvalidArgument("invalid cardinality: expected many")
|
||||
}
|
||||
type payload struct {
|
||||
Values []float64 `mapstructure:"floats" bson:"floats" json:"floats" yaml:"floats"`
|
||||
}
|
||||
var p payload
|
||||
if err := mapstructure.Decode(v.Values, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.Values, nil
|
||||
}
|
||||
|
||||
func (v *Value) AsDateTimes() ([]time.Time, error) {
|
||||
if v.Type != PTDateTime {
|
||||
return nil, invalidType(PTDateTime, v.Type)
|
||||
}
|
||||
if v.Cardinality != Many {
|
||||
return nil, merrors.InvalidArgument("invalid cardinality: expected many")
|
||||
}
|
||||
type payload struct {
|
||||
Values []time.Time `mapstructure:"date_times" bson:"date_times" json:"date_times" yaml:"date_times"`
|
||||
}
|
||||
var p payload
|
||||
if err := mapstructure.Decode(v.Values, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.Values, nil
|
||||
}
|
||||
|
||||
func (v *Value) AsMonetaries() ([]Money, error) {
|
||||
if v.Type != PTMonetary {
|
||||
return nil, invalidType(PTMonetary, v.Type)
|
||||
}
|
||||
if v.Cardinality != Many {
|
||||
return nil, merrors.InvalidArgument("invalid cardinality: expected many")
|
||||
}
|
||||
type payload struct {
|
||||
Values []Money `mapstructure:"monetaries" bson:"monetaries" json:"monetaries" yaml:"monetaries"`
|
||||
}
|
||||
var p payload
|
||||
if err := mapstructure.Decode(v.Values, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.Values, nil
|
||||
}
|
||||
|
||||
func (v *Value) AsReferences() ([]primitive.ObjectID, error) {
|
||||
if v.Type != PTReference {
|
||||
return nil, invalidType(PTReference, v.Type)
|
||||
}
|
||||
if v.Cardinality != Many {
|
||||
return nil, merrors.InvalidArgument("invalid cardinality: expected many")
|
||||
}
|
||||
type payload struct {
|
||||
Values []primitive.ObjectID `mapstructure:"references" bson:"references" json:"references" yaml:"references"`
|
||||
}
|
||||
var p payload
|
||||
if err := mapstructure.Decode(v.Values, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.Values, nil
|
||||
}
|
||||
|
||||
func (v *Value) AsObjects() ([]Object, error) {
|
||||
if v.Type != PTObject {
|
||||
return nil, invalidType(PTObject, v.Type)
|
||||
}
|
||||
if v.Cardinality != Many {
|
||||
return nil, merrors.InvalidArgument("invalid cardinality: expected many")
|
||||
}
|
||||
type payload struct {
|
||||
Values []Object `mapstructure:"objects" bson:"objects" json:"objects" yaml:"objects"`
|
||||
}
|
||||
var p payload
|
||||
if err := mapstructure.Decode(v.Values, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.Values, nil
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// FACTORIES (scheme + value)
|
||||
// ----------------------------
|
||||
|
||||
// Strings
|
||||
func NewStringValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, v string) (Value, error) {
|
||||
if scheme.Type != PTString {
|
||||
return Value{}, invalidType(PTString, scheme.Type)
|
||||
}
|
||||
if err := scheme.ValidateStrings([]string{v}); err != nil {
|
||||
return Value{}, err
|
||||
}
|
||||
return Value{
|
||||
PermissionBound: scope,
|
||||
Target: target,
|
||||
Type: PTString,
|
||||
Cardinality: One,
|
||||
PropertySchemaRef: scheme.ID,
|
||||
Values: SettingsT{VKString: v},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewStringsValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, vv []string) (Value, error) {
|
||||
if scheme.Type != PTString {
|
||||
return Value{}, invalidType(PTString, scheme.Type)
|
||||
}
|
||||
if err := scheme.ValidateStrings(vv); err != nil {
|
||||
return Value{}, err
|
||||
}
|
||||
return Value{
|
||||
PermissionBound: scope,
|
||||
Target: target,
|
||||
Type: PTString,
|
||||
Cardinality: Many,
|
||||
PropertySchemaRef: scheme.ID,
|
||||
Values: SettingsT{VKStrings: vv},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Colors
|
||||
func NewColorValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, v string) (Value, error) {
|
||||
if scheme.Type != PTColor {
|
||||
return Value{}, invalidType(PTColor, scheme.Type)
|
||||
}
|
||||
if err := scheme.ValidateColors([]string{v}); err != nil {
|
||||
return Value{}, err
|
||||
}
|
||||
return Value{scope, target, PTColor, One, scheme.ID, SettingsT{VKColor: v}}, nil
|
||||
}
|
||||
func NewColorsValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, vv []string) (Value, error) {
|
||||
if scheme.Type != PTColor {
|
||||
return Value{}, invalidType(PTColor, scheme.Type)
|
||||
}
|
||||
if err := scheme.ValidateColors(vv); err != nil {
|
||||
return Value{}, err
|
||||
}
|
||||
return Value{scope, target, PTColor, Many, scheme.ID, SettingsT{VKColors: vv}}, nil
|
||||
}
|
||||
|
||||
// Integers
|
||||
func NewIntegerValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, v int64) (Value, error) {
|
||||
if scheme.Type != PTInteger {
|
||||
return Value{}, invalidType(PTInteger, scheme.Type)
|
||||
}
|
||||
if err := scheme.ValidateIntegers([]int64{v}); err != nil {
|
||||
return Value{}, err
|
||||
}
|
||||
return Value{scope, target, PTInteger, One, scheme.ID, SettingsT{VKInteger: v}}, nil
|
||||
}
|
||||
func NewIntegersValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, vv []int64) (Value, error) {
|
||||
if scheme.Type != PTInteger {
|
||||
return Value{}, invalidType(PTInteger, scheme.Type)
|
||||
}
|
||||
if err := scheme.ValidateIntegers(vv); err != nil {
|
||||
return Value{}, err
|
||||
}
|
||||
return Value{scope, target, PTInteger, Many, scheme.ID, SettingsT{VKIntegers: vv}}, nil
|
||||
}
|
||||
|
||||
// Floats
|
||||
func NewFloatValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, v float64) (Value, error) {
|
||||
if scheme.Type != PTFloat {
|
||||
return Value{}, invalidType(PTFloat, scheme.Type)
|
||||
}
|
||||
if err := scheme.ValidateFloats([]float64{v}); err != nil {
|
||||
return Value{}, err
|
||||
}
|
||||
return Value{scope, target, PTFloat, One, scheme.ID, SettingsT{VKFloat: v}}, nil
|
||||
}
|
||||
func NewFloatsValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, vv []float64) (Value, error) {
|
||||
if scheme.Type != PTFloat {
|
||||
return Value{}, invalidType(PTFloat, scheme.Type)
|
||||
}
|
||||
if err := scheme.ValidateFloats(vv); err != nil {
|
||||
return Value{}, err
|
||||
}
|
||||
return Value{scope, target, PTFloat, Many, scheme.ID, SettingsT{VKFloats: vv}}, nil
|
||||
}
|
||||
|
||||
// DateTimes
|
||||
func NewDateTimeValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, v time.Time) (Value, error) {
|
||||
if scheme.Type != PTDateTime {
|
||||
return Value{}, invalidType(PTDateTime, scheme.Type)
|
||||
}
|
||||
if err := scheme.ValidateDateTimes([]time.Time{v}); err != nil {
|
||||
return Value{}, err
|
||||
}
|
||||
return Value{scope, target, PTDateTime, One, scheme.ID, SettingsT{VKDateTime: v}}, nil
|
||||
}
|
||||
func NewDateTimesValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, vv []time.Time) (Value, error) {
|
||||
if scheme.Type != PTDateTime {
|
||||
return Value{}, invalidType(PTDateTime, scheme.Type)
|
||||
}
|
||||
if err := scheme.ValidateDateTimes(vv); err != nil {
|
||||
return Value{}, err
|
||||
}
|
||||
return Value{scope, target, PTDateTime, Many, scheme.ID, SettingsT{VKDateTimes: vv}}, nil
|
||||
}
|
||||
|
||||
// Monetary (needs org currency for validation if required by scheme)
|
||||
func NewMonetaryValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, v Money, orgCurrency Currency) (Value, error) {
|
||||
if scheme.Type != PTMonetary {
|
||||
return Value{}, invalidType(PTMonetary, scheme.Type)
|
||||
}
|
||||
if err := scheme.ValidateMonetaries([]Money{v}, orgCurrency); err != nil {
|
||||
return Value{}, err
|
||||
}
|
||||
return Value{scope, target, PTMonetary, One, scheme.ID, SettingsT{VKMonetary: v}}, nil
|
||||
}
|
||||
func NewMonetariesValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, vv []Money, orgCurrency Currency) (Value, error) {
|
||||
if scheme.Type != PTMonetary {
|
||||
return Value{}, invalidType(PTMonetary, scheme.Type)
|
||||
}
|
||||
if err := scheme.ValidateMonetaries(vv, orgCurrency); err != nil {
|
||||
return Value{}, err
|
||||
}
|
||||
return Value{scope, target, PTMonetary, Many, scheme.ID, SettingsT{VKMonetaries: vv}}, nil
|
||||
}
|
||||
|
||||
// References (needs exist-fn)
|
||||
func NewReferenceValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, v primitive.ObjectID, exist ExistFn) (Value, error) {
|
||||
if scheme.Type != PTReference {
|
||||
return Value{}, invalidType(PTReference, scheme.Type)
|
||||
}
|
||||
if err := scheme.ValidateReferences([]primitive.ObjectID{v}, exist); err != nil {
|
||||
return Value{}, err
|
||||
}
|
||||
return Value{scope, target, PTReference, One, scheme.ID, SettingsT{VKReference: v}}, nil
|
||||
}
|
||||
func NewReferencesValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, vv []primitive.ObjectID, exist ExistFn) (Value, error) {
|
||||
if scheme.Type != PTReference {
|
||||
return Value{}, invalidType(PTReference, scheme.Type)
|
||||
}
|
||||
if err := scheme.ValidateReferences(vv, exist); err != nil {
|
||||
return Value{}, err
|
||||
}
|
||||
return Value{scope, target, PTReference, Many, scheme.ID, SettingsT{VKReferences: vv}}, nil
|
||||
}
|
||||
|
||||
// Objects (opaque maps)
|
||||
func NewObjectValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, v Object) (Value, error) {
|
||||
if scheme.Type != PTObject {
|
||||
return Value{}, invalidType(PTObject, scheme.Type)
|
||||
}
|
||||
// Add your own ValidateObject if needed
|
||||
return Value{scope, target, PTObject, One, scheme.ID, SettingsT{VKObject: v}}, nil
|
||||
}
|
||||
func NewObjectsValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, vv []Object) (Value, error) {
|
||||
if scheme.Type != PTObject {
|
||||
return Value{}, invalidType(PTObject, scheme.Type)
|
||||
}
|
||||
return Value{scope, target, PTObject, Many, scheme.ID, SettingsT{VKObjects: vv}}, nil
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Custom BSON Marshalers/Unmarshalers
|
||||
// ----------------------------
|
||||
|
||||
// MarshalBSON implements bson.Marshaler to ensure proper serialization
|
||||
func (v Value) MarshalBSON() ([]byte, error) {
|
||||
// Create a temporary struct that preserves the exact structure
|
||||
temp := struct {
|
||||
PermissionBound `bson:",inline"`
|
||||
Target ObjectRef `bson:"target"`
|
||||
Type PropertyType `bson:"type"`
|
||||
Cardinality Cardinality `bson:"cardinality"`
|
||||
PropertySchemaRef primitive.ObjectID `bson:"propertySchemaRef"`
|
||||
Values SettingsTWrapper `bson:"data"`
|
||||
}{
|
||||
PermissionBound: v.PermissionBound,
|
||||
Target: v.Target,
|
||||
Type: v.Type,
|
||||
Cardinality: v.Cardinality,
|
||||
PropertySchemaRef: v.PropertySchemaRef,
|
||||
Values: SettingsTWrapper(v.Values),
|
||||
}
|
||||
|
||||
return bson.Marshal(temp)
|
||||
}
|
||||
|
||||
// UnmarshalBSON implements bson.Unmarshaler to ensure proper deserialization
|
||||
func (v *Value) UnmarshalBSON(data []byte) error {
|
||||
// Create a temporary struct that matches the BSON structure
|
||||
temp := struct {
|
||||
PermissionBound `bson:",inline"`
|
||||
Target ObjectRef `bson:"target"`
|
||||
Type PropertyType `bson:"type"`
|
||||
Cardinality Cardinality `bson:"cardinality"`
|
||||
PropertySchemaRef primitive.ObjectID `bson:"propertySchemaRef"`
|
||||
Values SettingsTWrapper `bson:"data"`
|
||||
}{}
|
||||
|
||||
if err := bson.Unmarshal(data, &temp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy the values back to the original struct
|
||||
v.PermissionBound = temp.PermissionBound
|
||||
v.Target = temp.Target
|
||||
v.Type = temp.Type
|
||||
v.Cardinality = temp.Cardinality
|
||||
v.PropertySchemaRef = temp.PropertySchemaRef
|
||||
v.Values = SettingsT(temp.Values)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Custom BSON Marshalers for SettingsT
|
||||
// ----------------------------
|
||||
|
||||
// SettingsT is a type alias, so we need to define a wrapper type for methods
|
||||
type SettingsTWrapper SettingsT
|
||||
|
||||
// MarshalBSON implements bson.Marshaler for SettingsT to preserve exact types
|
||||
func (s SettingsTWrapper) MarshalBSON() ([]byte, error) {
|
||||
// Convert SettingsT to bson.M to preserve exact types
|
||||
doc := bson.M{}
|
||||
for key, value := range s {
|
||||
doc[key] = value
|
||||
}
|
||||
return bson.Marshal(doc)
|
||||
}
|
||||
|
||||
// UnmarshalBSON implements bson.Unmarshaler for SettingsT to preserve exact types
|
||||
func (s *SettingsTWrapper) UnmarshalBSON(data []byte) error {
|
||||
// Unmarshal into a generic map first
|
||||
var doc bson.M
|
||||
if err := bson.Unmarshal(data, &doc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert back to SettingsT, preserving types
|
||||
*s = make(SettingsT)
|
||||
for key, value := range doc {
|
||||
// Handle special cases where BSON converts types
|
||||
switch v := value.(type) {
|
||||
case primitive.A:
|
||||
// Convert primitive.A back to appropriate slice type
|
||||
if len(v) > 0 {
|
||||
switch v[0].(type) {
|
||||
case string:
|
||||
strings := make([]string, len(v))
|
||||
for i, item := range v {
|
||||
strings[i] = item.(string)
|
||||
}
|
||||
(*s)[key] = strings
|
||||
case int32, int64:
|
||||
ints := make([]int64, len(v))
|
||||
for i, item := range v {
|
||||
switch val := item.(type) {
|
||||
case int32:
|
||||
ints[i] = int64(val)
|
||||
case int64:
|
||||
ints[i] = val
|
||||
}
|
||||
}
|
||||
(*s)[key] = ints
|
||||
case float32, float64:
|
||||
floats := make([]float64, len(v))
|
||||
for i, item := range v {
|
||||
switch val := item.(type) {
|
||||
case float32:
|
||||
floats[i] = float64(val)
|
||||
case float64:
|
||||
floats[i] = val
|
||||
}
|
||||
}
|
||||
(*s)[key] = floats
|
||||
case primitive.DateTime:
|
||||
times := make([]time.Time, len(v))
|
||||
for i, item := range v {
|
||||
times[i] = item.(primitive.DateTime).Time().Truncate(time.Millisecond)
|
||||
}
|
||||
(*s)[key] = times
|
||||
case primitive.ObjectID:
|
||||
refs := make([]primitive.ObjectID, len(v))
|
||||
for i, item := range v {
|
||||
refs[i] = item.(primitive.ObjectID)
|
||||
}
|
||||
(*s)[key] = refs
|
||||
case bson.M:
|
||||
// Handle nested objects (Money, Object, etc.)
|
||||
if key == VKMonetaries {
|
||||
// Handle Money slice
|
||||
moneys := make([]Money, len(v))
|
||||
for i, item := range v {
|
||||
if itemMap, ok := item.(bson.M); ok {
|
||||
var money Money
|
||||
if amount, ok := itemMap[MKAmount].(primitive.Decimal128); ok {
|
||||
money.Amount = amount
|
||||
}
|
||||
if currency, ok := itemMap[MKCurrency].(string); ok {
|
||||
money.Currency = Currency(currency)
|
||||
}
|
||||
moneys[i] = money
|
||||
}
|
||||
}
|
||||
(*s)[key] = moneys
|
||||
} else {
|
||||
// Handle Object slice
|
||||
objects := make([]Object, len(v))
|
||||
for i, item := range v {
|
||||
obj := make(Object)
|
||||
for k, val := range item.(bson.M) {
|
||||
// Recursively handle nested Values
|
||||
if valMap, ok := val.(bson.M); ok {
|
||||
var nestedValue Value
|
||||
if data, err := bson.Marshal(valMap); err == nil {
|
||||
if err := bson.Unmarshal(data, &nestedValue); err == nil {
|
||||
obj[k] = nestedValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
objects[i] = obj
|
||||
}
|
||||
(*s)[key] = objects
|
||||
}
|
||||
default:
|
||||
// Fallback: keep as primitive.A
|
||||
(*s)[key] = v
|
||||
}
|
||||
} else {
|
||||
// Empty array - determine type from key name
|
||||
switch key {
|
||||
case VKStrings, VKColors:
|
||||
(*s)[key] = []string{}
|
||||
case VKIntegers:
|
||||
(*s)[key] = []int64{}
|
||||
case VKFloats:
|
||||
(*s)[key] = []float64{}
|
||||
case VKDateTimes:
|
||||
(*s)[key] = []time.Time{}
|
||||
case VKReferences:
|
||||
(*s)[key] = []primitive.ObjectID{}
|
||||
case VKMonetaries:
|
||||
(*s)[key] = []Money{}
|
||||
case VKObjects:
|
||||
(*s)[key] = []Object{}
|
||||
default:
|
||||
(*s)[key] = []interface{}{}
|
||||
}
|
||||
}
|
||||
case primitive.DateTime:
|
||||
// Convert primitive.DateTime back to time.Time and truncate to millisecond precision
|
||||
(*s)[key] = v.Time().Truncate(time.Millisecond)
|
||||
case int64:
|
||||
// Handle time.Time that gets converted to int64 (Unix timestamp)
|
||||
if key == VKDateTime {
|
||||
(*s)[key] = time.Unix(v, 0).UTC().Truncate(time.Millisecond)
|
||||
} else {
|
||||
(*s)[key] = v
|
||||
}
|
||||
case bson.M:
|
||||
// Handle nested objects
|
||||
if key == VKMonetary {
|
||||
// Handle Money struct
|
||||
var money Money
|
||||
if amount, ok := v[MKAmount].(primitive.Decimal128); ok {
|
||||
money.Amount = amount
|
||||
}
|
||||
if currency, ok := v[MKCurrency].(string); ok {
|
||||
money.Currency = Currency(currency)
|
||||
}
|
||||
(*s)[key] = money
|
||||
} else if key == VKMonetaries {
|
||||
// Handle Money slice - this shouldn't happen in single values
|
||||
(*s)[key] = v
|
||||
} else if key == VKObject {
|
||||
// Handle Object type
|
||||
obj := make(Object)
|
||||
for k, val := range v {
|
||||
if valMap, ok := val.(bson.M); ok {
|
||||
var nestedValue Value
|
||||
if data, err := bson.Marshal(valMap); err == nil {
|
||||
if err := bson.Unmarshal(data, &nestedValue); err == nil {
|
||||
obj[k] = nestedValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(*s)[key] = obj
|
||||
} else {
|
||||
// Generic map
|
||||
(*s)[key] = v
|
||||
}
|
||||
case nil:
|
||||
// Handle nil values - determine type from key name
|
||||
switch key {
|
||||
case VKStrings, VKColors:
|
||||
(*s)[key] = []string(nil)
|
||||
case VKIntegers:
|
||||
(*s)[key] = []int64(nil)
|
||||
case VKFloats:
|
||||
(*s)[key] = []float64(nil)
|
||||
case VKDateTimes:
|
||||
(*s)[key] = []time.Time(nil)
|
||||
case VKReferences:
|
||||
(*s)[key] = []primitive.ObjectID(nil)
|
||||
case VKMonetaries:
|
||||
(*s)[key] = []Money(nil)
|
||||
case VKObjects:
|
||||
(*s)[key] = []Object(nil)
|
||||
default:
|
||||
(*s)[key] = nil
|
||||
}
|
||||
default:
|
||||
// Keep as-is for primitive types
|
||||
(*s)[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
1397
api/pkg/model/value_test.go
Normal file
1397
api/pkg/model/value_test.go
Normal file
File diff suppressed because it is too large
Load Diff
8
api/pkg/model/viewcursor.go
Normal file
8
api/pkg/model/viewcursor.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package model
|
||||
|
||||
// ViewCursor aggregates pagination and archival filters for list queries
|
||||
type ViewCursor struct {
|
||||
Limit *int64
|
||||
Offset *int64
|
||||
IsArchived *bool
|
||||
}
|
||||
19
api/pkg/model/workflow.go
Normal file
19
api/pkg/model/workflow.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type Workflow struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
ArchivableBase `bson:",inline" json:",inline"`
|
||||
Describable `bson:",inline" json:",inline"`
|
||||
Priorities []primitive.ObjectID `bson:"priorities" json:"priorities"` // Ordered list of StepRefs
|
||||
Steps []primitive.ObjectID `bson:"steps" json:"steps"` // Ordered list of StepRefs
|
||||
}
|
||||
|
||||
func (*Workflow) Collection() string {
|
||||
return mservice.Workflows
|
||||
}
|
||||
17
api/pkg/model/workspace.go
Normal file
17
api/pkg/model/workspace.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type Workspace struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
Describable `bson:",inline" json:",inline"`
|
||||
Projects []primitive.ObjectID `bson:"projects" json:"projects"` // References to projects in the workspace
|
||||
}
|
||||
|
||||
func (*Workspace) Collection() string {
|
||||
return mservice.Workspaces
|
||||
}
|
||||
Reference in New Issue
Block a user