多租户更改
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
// Package adapter contains the interfaces to be implemented by the database adapter
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/tinode/chat/server/auth"
|
||||
t "github.com/tinode/chat/server/store/types"
|
||||
)
|
||||
|
||||
// Adapter is the interface that must be implemented by a database
|
||||
// adapter. The current schema supports a single connection by database type.
|
||||
type Adapter interface {
|
||||
// General
|
||||
|
||||
// Open and configure the adapter
|
||||
Open(config json.RawMessage) error
|
||||
// Close the adapter
|
||||
Close() error
|
||||
// IsOpen checks if the adapter is ready for use
|
||||
IsOpen() bool
|
||||
// GetDbVersion returns current database version.
|
||||
GetDbVersion() (int, error)
|
||||
// CheckDbVersion checks if the actual database version matches adapter version.
|
||||
CheckDbVersion() error
|
||||
// GetName returns the name of the adapter
|
||||
GetName() string
|
||||
// SetMaxResults configures how many results can be returned in a single DB call.
|
||||
SetMaxResults(val int) error
|
||||
// CreateDb creates the database optionally dropping an existing database first.
|
||||
CreateDb(reset bool) error
|
||||
// UpgradeDb upgrades database to the current adapter version.
|
||||
UpgradeDb() error
|
||||
// Version returns adapter version
|
||||
Version() int
|
||||
// DB connection stats object.
|
||||
Stats() any
|
||||
|
||||
// User management
|
||||
|
||||
// UserCreate creates user record
|
||||
UserCreate(user *t.User) error
|
||||
// UserGet returns record for a given user ID
|
||||
UserGet(uid t.Uid) (*t.User, error)
|
||||
// UserGetAll returns user records for a given list of user IDs
|
||||
UserGetAll(ids ...t.Uid) ([]t.User, error)
|
||||
// UserDelete deletes user record
|
||||
UserDelete(uid t.Uid, hard bool) error
|
||||
// UserUpdate updates user record
|
||||
UserUpdate(uid t.Uid, update map[string]any) error
|
||||
// UserUpdateTags adds, removes, or resets user's tags
|
||||
UserUpdateTags(uid t.Uid, add, remove, reset []string) ([]string, error)
|
||||
// UserGetByCred returns user ID for the given validated credential.
|
||||
UserGetByCred(method, value string) (t.Uid, error)
|
||||
// UserUnreadCount returns the total number of unread messages in all topics with
|
||||
// the R permission. If read fails, the counts are still returned with the original
|
||||
// user IDs but with the unread count undefined and non-nil error.
|
||||
UserUnreadCount(ids ...t.Uid) (map[t.Uid]int, error)
|
||||
// UserGetUnvalidated returns a list of no more than 'limit' uids who never logged in,
|
||||
// have no validated credentials and which haven't been updated since 'lastUpdatedBefore'.
|
||||
UserGetUnvalidated(lastUpdatedBefore time.Time, limit int) ([]t.Uid, error)
|
||||
|
||||
// Credential management
|
||||
|
||||
// CredUpsert adds or updates a credential record. Returns true if record was inserted, false if updated.
|
||||
CredUpsert(cred *t.Credential) (bool, error)
|
||||
// CredGetActive returns the currently active credential record for the given method.
|
||||
CredGetActive(uid t.Uid, method string) (*t.Credential, error)
|
||||
// CredGetAll returns credential records for the given user and method, validated only or all.
|
||||
CredGetAll(uid t.Uid, method string, validatedOnly bool) ([]t.Credential, error)
|
||||
// CredDel deletes credentials for the given method/value. If method is empty, deletes all
|
||||
// user's credentials.
|
||||
CredDel(uid t.Uid, method, value string) error
|
||||
// CredConfirm marks given credential as validated.
|
||||
CredConfirm(uid t.Uid, method string) error
|
||||
// CredFail increments count of failed validation attepmts for the given credentials.
|
||||
CredFail(uid t.Uid, method string) error
|
||||
|
||||
// Authentication management for the basic authentication scheme
|
||||
|
||||
// AuthGetUniqueRecord returns authentication record for a given unique value i.e. login.
|
||||
AuthGetUniqueRecord(unique string) (t.Uid, auth.Level, []byte, time.Time, error)
|
||||
// AuthGetRecord returns authentication record given user ID and method.
|
||||
AuthGetRecord(user t.Uid, scheme string) (string, auth.Level, []byte, time.Time, error)
|
||||
// AuthAddRecord creates new authentication record
|
||||
AuthAddRecord(user t.Uid, scheme, unique string, authLvl auth.Level, secret []byte, expires time.Time) error
|
||||
// AuthDelScheme deletes an existing authentication scheme for the user.
|
||||
AuthDelScheme(user t.Uid, scheme string) error
|
||||
// AuthDelAllRecords deletes all records of a given user.
|
||||
AuthDelAllRecords(uid t.Uid) (int, error)
|
||||
// AuthUpdRecord modifies an authentication record. Only non-default/non-zero values are updated.
|
||||
AuthUpdRecord(user t.Uid, scheme, unique string, authLvl auth.Level, secret []byte, expires time.Time) error
|
||||
|
||||
// Topic management
|
||||
|
||||
// TopicCreate creates a topic
|
||||
TopicCreate(topic *t.Topic) error
|
||||
// TopicCreateP2P creates a p2p topic
|
||||
TopicCreateP2P(initiator, invited *t.Subscription) error
|
||||
// TopicGet loads a single topic by name, if it exists. If the topic does not exist the call returns (nil, nil)
|
||||
TopicGet(topic string) (*t.Topic, error)
|
||||
// TopicsForUser loads subscriptions for a given user. Reads public value.
|
||||
// When the 'opts.IfModifiedSince' query is not nil the subscriptions with UpdatedAt > opts.IfModifiedSince
|
||||
// are returned, where UpdatedAt can be either a subscription, a topic, or a user update timestamp.
|
||||
// This is need in order to support paginagion of subscriptions: get subscriptions page by page
|
||||
// from the oldest updates to most recent:
|
||||
// 1. Client already has subscriptions with the latest update timestamp X.
|
||||
// 2. Client asks for N updated subscriptions since X. The server returns N with updates between X and Y.
|
||||
// 3. Client goes to step 1 with X := Y.
|
||||
TopicsForUser(uid t.Uid, keepDeleted bool, opts *t.QueryOpt) ([]t.Subscription, error)
|
||||
// UsersForTopic loads users' subscriptions for a given topic. Public is loaded.
|
||||
UsersForTopic(topic string, keepDeleted bool, opts *t.QueryOpt) ([]t.Subscription, error)
|
||||
// OwnTopics loads a slice of topic names where the user is the owner.
|
||||
OwnTopics(uid t.Uid) ([]string, error)
|
||||
// ChannelsForUser loads a slice of topic names where the user is a channel reader and notifications (P) are enabled.
|
||||
ChannelsForUser(uid t.Uid) ([]string, error)
|
||||
// TopicShare creates topic subscriptions.
|
||||
TopicShare(topic string, subs []*t.Subscription) error
|
||||
// TopicDelete deletes topic, subscriptions, messages.
|
||||
TopicDelete(topic string, isChan, hard bool) error
|
||||
// TopicUpdateOnMessage increments Topic's or User's SeqId value and updates TouchedAt timestamp.
|
||||
TopicUpdateOnMessage(topic string, msg *t.Message) error
|
||||
// TopicUpdateSubCnt refreshes denormalized topic subscriber count.
|
||||
TopicUpdateSubCnt(topic string) error
|
||||
// TopicUpdate updates topic record.
|
||||
TopicUpdate(topic string, update map[string]any) error
|
||||
// TopicOwnerChange updates topic's owner
|
||||
TopicOwnerChange(topic string, newOwner t.Uid) error
|
||||
|
||||
// Topic subscriptions
|
||||
|
||||
// SubscriptionGet reads a subscription of a user to a topic
|
||||
SubscriptionGet(topic string, user t.Uid, keepDeleted bool) (*t.Subscription, error)
|
||||
// SubsForUser loads all subscriptions of a given user. Does NOT load Public or Private values,
|
||||
// does not load deleted subscriptions.
|
||||
SubsForUser(user t.Uid) ([]t.Subscription, error)
|
||||
// SubsForTopic gets a list of subscriptions to a given topic.. Does NOT load Public value.
|
||||
SubsForTopic(topic string, keepDeleted bool, opts *t.QueryOpt) ([]t.Subscription, error)
|
||||
// SubsUpdate updates pasrt of a subscription object. Pass nil for fields which don't need to be updated
|
||||
SubsUpdate(topic string, user t.Uid, update map[string]any) error
|
||||
// SubsDelete deletes a single subscription
|
||||
SubsDelete(topic string, user t.Uid) error
|
||||
|
||||
// Search
|
||||
|
||||
// Find searches for users or topics given a list of tags.
|
||||
// - caller is the user or topic who is doing the searching, it will be skipped from results.
|
||||
// - prefix if present will cause match rank highest in the results.
|
||||
// - req is a list of required tag sets. Each set is a list of tags. The search will return
|
||||
// all users/topics which have at least one tag from each set.
|
||||
// - opt is a list of optional tags; if present the result will rank higher.
|
||||
// - activeOnly if true will return only active subscriptions.
|
||||
Find(caller, prefix string, req [][]string, opt []string, activeOnly bool) ([]t.Subscription, error)
|
||||
// FindOne returns topic or user which matches the given tag.
|
||||
FindOne(tag string) (string, error)
|
||||
|
||||
// Messages
|
||||
|
||||
// MessageSave saves message to database
|
||||
MessageSave(msg *t.Message) error
|
||||
// MessageGetAll returns messages matching the query
|
||||
MessageGetAll(topic string, forUser t.Uid, opts *t.QueryOpt) ([]t.Message, error)
|
||||
// MessageDeleteList marks messages as deleted.
|
||||
// Soft- or Hard- is defined by forUser value: forUser.IsZero == true is hard.
|
||||
MessageDeleteList(topic string, toDel *t.DelMessage) error
|
||||
// MessageGetDeleted returns a list of deleted message Ids.
|
||||
MessageGetDeleted(topic string, forUser t.Uid, opts *t.QueryOpt) ([]t.DelMessage, error)
|
||||
|
||||
// Devices (for push notifications)
|
||||
|
||||
// DeviceUpsert creates or updates a device record
|
||||
DeviceUpsert(uid t.Uid, dev *t.DeviceDef) error
|
||||
// DeviceGetAll returns all devices for a given set of users
|
||||
DeviceGetAll(uid ...t.Uid) (map[t.Uid][]t.DeviceDef, int, error)
|
||||
// DeviceDelete deletes a device record
|
||||
DeviceDelete(uid t.Uid, deviceID string) error
|
||||
|
||||
// File upload records. The files are stored outside of the database.
|
||||
|
||||
// FileStartUpload initializes a file upload.
|
||||
FileStartUpload(fd *t.FileDef) error
|
||||
// FileFinishUpload marks file upload as completed, successfully or otherwise.
|
||||
FileFinishUpload(fd *t.FileDef, success bool, size int64) (*t.FileDef, error)
|
||||
// FileGet fetches a record of a specific file
|
||||
FileGet(fid string) (*t.FileDef, error)
|
||||
// FileDeleteUnused deletes records where UseCount is zero. If olderThan is non-zero, deletes
|
||||
// unused records with UpdatedAt before olderThan.
|
||||
// Returns array of FileDef.Location of deleted filerecords so actual files can be deleted too.
|
||||
FileDeleteUnused(olderThan time.Time, limit int) ([]string, error)
|
||||
// FileLinkAttachments connects given topic or message to the file record IDs from the list.
|
||||
FileLinkAttachments(topic string, userId, msgId t.Uid, fids []string) error
|
||||
|
||||
// Persistent cache management.
|
||||
|
||||
// PCacheGet reads a persistent cache entry.
|
||||
PCacheGet(key string) (string, error)
|
||||
// PCacheUpsert creates or updates a persistent cache entry.
|
||||
PCacheUpsert(key string, value string, failOnDuplicate bool) error
|
||||
// PCacheDelete deletes a single persistent cache entry.
|
||||
PCacheDelete(key string) error
|
||||
// PCacheExpire expires older entries with the specified key prefix.
|
||||
PCacheExpire(keyPrefix string, olderThan time.Time) error
|
||||
|
||||
// Testing
|
||||
|
||||
// GetTestDB returns a currently open database connection.
|
||||
GetTestDB() any
|
||||
}
|
||||
|
||||
// TenantAdapter is an optional capability implemented by tenant-aware database adapters.
|
||||
// Keeping it separate lets adapters be migrated independently while tenant-aware entry
|
||||
// points fail closed when the configured adapter does not support tenant resolution.
|
||||
type TenantAdapter interface {
|
||||
TenantGetByCode(code string) (*t.Tenant, error)
|
||||
}
|
||||
|
||||
// TenantAuthAdapter is the tenant-aware authentication and credential capability.
|
||||
// It is separate from Adapter while the remaining persistence domains are migrated.
|
||||
type TenantAuthAdapter interface {
|
||||
TenantAuthGetUniqueRecord(tenantID t.TenantID, unique string) (t.Uid, auth.Level, []byte, time.Time, error)
|
||||
TenantAuthGetRecord(tenantID t.TenantID, user t.Uid, scheme string) (string, auth.Level, []byte, time.Time, error)
|
||||
TenantAuthAddRecord(tenantID t.TenantID, user t.Uid, scheme, unique string, authLvl auth.Level,
|
||||
secret []byte, expires time.Time) error
|
||||
TenantAuthDelScheme(tenantID t.TenantID, user t.Uid, scheme string) error
|
||||
TenantAuthUpdRecord(tenantID t.TenantID, user t.Uid, scheme, unique string, authLvl auth.Level,
|
||||
secret []byte, expires time.Time) error
|
||||
TenantUserGetByCred(tenantID t.TenantID, method, value string) (t.Uid, error)
|
||||
TenantCredUpsert(tenantID t.TenantID, cred *t.Credential) (bool, error)
|
||||
TenantCredGetActive(tenantID t.TenantID, uid t.Uid, method string) (*t.Credential, error)
|
||||
TenantCredGetAll(tenantID t.TenantID, uid t.Uid, method string, validatedOnly bool) ([]t.Credential, error)
|
||||
TenantCredDel(tenantID t.TenantID, uid t.Uid, method, value string) error
|
||||
TenantCredConfirm(tenantID t.TenantID, uid t.Uid, method string) error
|
||||
TenantCredFail(tenantID t.TenantID, uid t.Uid, method string) error
|
||||
}
|
||||
|
||||
// TenantBusinessAdapter is the mandatory tenant-aware persistence capability
|
||||
// used by request handling code. Adapters which do not implement it fail closed
|
||||
// at the Store boundary instead of falling back to legacy unscoped queries.
|
||||
type TenantBusinessAdapter interface {
|
||||
// TenantBusinessReady reports whether every method below enforces tenant
|
||||
// isolation in its database queries. Startup is rejected while false.
|
||||
TenantBusinessReady() bool
|
||||
|
||||
TenantUserCreate(t.TenantID, *t.User) error
|
||||
TenantUserGet(t.TenantID, t.Uid) (*t.User, error)
|
||||
TenantUserGetAll(t.TenantID, ...t.Uid) ([]t.User, error)
|
||||
TenantUserDelete(t.TenantID, t.Uid, bool) error
|
||||
TenantUserUpdate(t.TenantID, t.Uid, map[string]any) error
|
||||
TenantUserUpdateTags(t.TenantID, t.Uid, []string, []string, []string) ([]string, error)
|
||||
TenantUserUnreadCount(t.TenantID, ...t.Uid) (map[t.Uid]int, error)
|
||||
TenantUserGetUnvalidated(t.TenantID, time.Time, int) ([]t.Uid, error)
|
||||
|
||||
TenantTopicCreate(t.TenantID, *t.Topic) error
|
||||
TenantTopicCreateP2P(t.TenantID, *t.Subscription, *t.Subscription) error
|
||||
TenantTopicGet(t.TenantID, string) (*t.Topic, error)
|
||||
TenantTopicsForUser(t.TenantID, t.Uid, bool, *t.QueryOpt) ([]t.Subscription, error)
|
||||
TenantUsersForTopic(t.TenantID, string, bool, *t.QueryOpt) ([]t.Subscription, error)
|
||||
TenantOwnTopics(t.TenantID, t.Uid) ([]string, error)
|
||||
TenantChannelsForUser(t.TenantID, t.Uid) ([]string, error)
|
||||
TenantTopicShare(t.TenantID, string, []*t.Subscription) error
|
||||
TenantTopicDelete(t.TenantID, string, bool, bool) error
|
||||
TenantTopicUpdateOnMessage(t.TenantID, string, *t.Message) error
|
||||
TenantTopicUpdateSubCnt(t.TenantID, string) error
|
||||
TenantTopicUpdate(t.TenantID, string, map[string]any) error
|
||||
TenantTopicOwnerChange(t.TenantID, string, t.Uid) error
|
||||
|
||||
TenantSubscriptionGet(t.TenantID, string, t.Uid, bool) (*t.Subscription, error)
|
||||
TenantSubsForUser(t.TenantID, t.Uid) ([]t.Subscription, error)
|
||||
TenantSubsForTopic(t.TenantID, string, bool, *t.QueryOpt) ([]t.Subscription, error)
|
||||
TenantSubsUpdate(t.TenantID, string, t.Uid, map[string]any) error
|
||||
TenantSubsDelete(t.TenantID, string, t.Uid) error
|
||||
TenantFind(t.TenantID, string, string, [][]string, []string, bool) ([]t.Subscription, error)
|
||||
TenantFindOne(t.TenantID, string) (string, error)
|
||||
|
||||
TenantMessageSave(t.TenantID, *t.Message) error
|
||||
TenantMessageGetAll(t.TenantID, string, t.Uid, *t.QueryOpt) ([]t.Message, error)
|
||||
TenantMessageDeleteList(t.TenantID, string, *t.DelMessage) error
|
||||
TenantMessageGetDeleted(t.TenantID, string, t.Uid, *t.QueryOpt) ([]t.DelMessage, error)
|
||||
|
||||
TenantDeviceUpsert(t.TenantID, t.Uid, *t.DeviceDef) error
|
||||
TenantDeviceGetAll(t.TenantID, ...t.Uid) (map[t.Uid][]t.DeviceDef, int, error)
|
||||
TenantDeviceDelete(t.TenantID, t.Uid, string) error
|
||||
|
||||
TenantFileStartUpload(t.TenantID, *t.FileDef) error
|
||||
TenantFileFinishUpload(t.TenantID, *t.FileDef, bool, int64) (*t.FileDef, error)
|
||||
TenantFileGet(t.TenantID, string) (*t.FileDef, error)
|
||||
TenantFileDeleteUnused(t.TenantID, time.Time, int) ([]string, error)
|
||||
TenantFileLinkAttachments(t.TenantID, string, t.Uid, t.Uid, []string) error
|
||||
|
||||
TenantPCacheGet(t.TenantID, string) (string, error)
|
||||
TenantPCacheUpsert(t.TenantID, string, string, bool) error
|
||||
TenantPCacheDelete(t.TenantID, string) error
|
||||
TenantPCacheExpire(t.TenantID, string, time.Time) error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
// Package common contains utility methods used by all adapters.
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tinode/chat/server/auth"
|
||||
"github.com/tinode/chat/server/store"
|
||||
t "github.com/tinode/chat/server/store/types"
|
||||
)
|
||||
|
||||
type AuthRecord struct {
|
||||
Unique string `json:"unique" bson:"_id"`
|
||||
UserId string `json:"userid"`
|
||||
Scheme string `json:"scheme"`
|
||||
AuthLvl auth.Level `json:"authLvl"`
|
||||
Secret []byte `json:"secret"`
|
||||
Expires time.Time `json:"expires"`
|
||||
}
|
||||
|
||||
// SelectEarliestUpdatedSubs selects no more than the given number of subscriptions from the
|
||||
// given slice satisfying the query. When the number of subscriptions is greater than the limit,
|
||||
// the subscriptions with the earliest timestamp are selected.
|
||||
func SelectEarliestUpdatedSubs(subs []t.Subscription, opts *t.QueryOpt, maxResults int) []t.Subscription {
|
||||
limit := maxResults
|
||||
ims := time.Time{}
|
||||
if opts != nil {
|
||||
if opts.Limit > 0 && opts.Limit < limit {
|
||||
limit = opts.Limit
|
||||
}
|
||||
if opts.IfModifiedSince != nil {
|
||||
ims = *opts.IfModifiedSince
|
||||
}
|
||||
}
|
||||
|
||||
// No cache management and the number of results is below the limit: return all.
|
||||
if ims.IsZero() && len(subs) <= limit {
|
||||
return subs
|
||||
}
|
||||
|
||||
// Now that we fetched potentially more subscriptions than needed, we got to take those with the oldest modifications.
|
||||
// Sorting in ascending order by modification time.
|
||||
sort.Slice(subs, func(i, j int) bool {
|
||||
return subs[i].LastModified().Before(subs[j].LastModified())
|
||||
})
|
||||
|
||||
if !ims.IsZero() {
|
||||
// Keep only those subscriptions which are newer than ims.
|
||||
at := sort.Search(len(subs), func(i int) bool {
|
||||
return subs[i].LastModified().After(ims)
|
||||
})
|
||||
subs = subs[at:]
|
||||
}
|
||||
// Trim slice at the limit.
|
||||
if len(subs) > limit {
|
||||
subs = subs[:limit]
|
||||
}
|
||||
|
||||
return subs
|
||||
}
|
||||
|
||||
// SelectLatestTime picks the latest update timestamp out of the two.
|
||||
func SelectLatestTime(t1, t2 time.Time) time.Time {
|
||||
if t1.Before(t2) {
|
||||
// Subscription has not changed recently, use user's update timestamp.
|
||||
return t2
|
||||
}
|
||||
|
||||
return t1
|
||||
}
|
||||
|
||||
// RangesToSql converts a slice of ranges to SQL BETWEEN or IN() constraint and arguments.
|
||||
func RangesToSql(in []t.Range) (string, []any) {
|
||||
if len(in) > 1 || in[0].Hi == 0 {
|
||||
var args []any
|
||||
for _, r := range in {
|
||||
if r.Hi == 0 {
|
||||
args = append(args, r.Low)
|
||||
} else {
|
||||
for i := r.Low; i < r.Hi; i++ {
|
||||
args = append(args, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "IN (?" + strings.Repeat(",?", len(args)-1) + ")", args
|
||||
}
|
||||
|
||||
// Optimizing for a special case of single range low..hi.
|
||||
// SQL's BETWEEN is inclusive-inclusive thus decrement Hi by 1.
|
||||
return "BETWEEN ? AND ?", []any{in[0].Low, in[0].Hi - 1}
|
||||
}
|
||||
|
||||
// DisjunctionSql converts a slice of disjunctions to SQL HAVING clause and arguments.
|
||||
func DisjunctionSql(req [][]string, fieldName string) (string, []any) {
|
||||
var args []any
|
||||
counts := make([]string, 0, len(req))
|
||||
for _, reqDisjunction := range req {
|
||||
// At least one of the tags must be present.
|
||||
if len(reqDisjunction) == 0 {
|
||||
continue
|
||||
}
|
||||
counts = append(counts, "COUNT("+fieldName+" IN (?"+strings.Repeat(",?", len(reqDisjunction)-1)+") OR NULL)>=1")
|
||||
for _, tag := range reqDisjunction {
|
||||
args = append(args, tag)
|
||||
}
|
||||
}
|
||||
return "HAVING " + strings.Join(counts, " AND ") + " ", args
|
||||
}
|
||||
|
||||
// FilterFoundTags keeps only those tags in setTags that are present in the index.
|
||||
func FilterFoundTags(setTags t.StringSlice, index map[string]struct{}) []string {
|
||||
foundTags := make([]string, 0, 1)
|
||||
for _, tag := range setTags {
|
||||
if _, ok := index[tag]; ok {
|
||||
foundTags = append(foundTags, tag)
|
||||
}
|
||||
}
|
||||
return foundTags
|
||||
}
|
||||
|
||||
// Convert to JSON before storing to JSON field.
|
||||
func ToJSON(src any) []byte {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
jval, _ := json.Marshal(src)
|
||||
return jval
|
||||
}
|
||||
|
||||
// Deserialize JSON data from DB.
|
||||
func FromJSON(src any) any {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
if bb, ok := src.([]byte); ok {
|
||||
var out any
|
||||
json.Unmarshal(bb, &out)
|
||||
return out
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert update to a list of columns and arguments.
|
||||
func UpdateByMap(update map[string]any) (cols []string, args []any) {
|
||||
for col, arg := range update {
|
||||
col = strings.ToLower(col)
|
||||
if col == "public" || col == "trusted" || col == "private" || col == "aux" {
|
||||
arg = ToJSON(arg)
|
||||
}
|
||||
cols = append(cols, col+"=?")
|
||||
args = append(args, arg)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// If Tags field is updated, get the tags so tags table cab be updated too.
|
||||
func ExtractTags(update map[string]any) []string {
|
||||
var tags []string
|
||||
|
||||
if val := update["Tags"]; val != nil {
|
||||
tags, _ = val.(t.StringSlice)
|
||||
}
|
||||
|
||||
return []string(tags)
|
||||
}
|
||||
|
||||
// EncodeUidString takes decoded string representation of int64, produce UID.
|
||||
// UIDs are stored as decoded int64 values.
|
||||
func EncodeUidString(str string) t.Uid {
|
||||
unum, _ := strconv.ParseInt(str, 10, 64)
|
||||
return store.EncodeUid(unum)
|
||||
}
|
||||
|
||||
// DecodeUidString takes UID as string, converts it to int64 representation.
|
||||
// UIDs are stored as decoded int64 values.
|
||||
func DecodeUidString(str string) int64 {
|
||||
uid := t.ParseUid(str)
|
||||
return store.DecodeUid(uid)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tinode/chat/server/store/types"
|
||||
)
|
||||
|
||||
func genTestData() []types.Subscription {
|
||||
var testData = []types.Subscription{
|
||||
{ObjHeader: types.ObjHeader{Id: "1", UpdatedAt: time.Date(2021, time.June, 1, 1, 11, 0, 0, time.Local)}},
|
||||
{ObjHeader: types.ObjHeader{Id: "2", UpdatedAt: time.Date(2021, time.June, 2, 2, 12, 0, 0, time.Local)}},
|
||||
{ObjHeader: types.ObjHeader{Id: "3", UpdatedAt: time.Date(2021, time.June, 3, 3, 13, 0, 0, time.Local)}},
|
||||
{ObjHeader: types.ObjHeader{Id: "4", UpdatedAt: time.Date(2021, time.June, 4, 4, 14, 0, 0, time.Local)}},
|
||||
{ObjHeader: types.ObjHeader{Id: "5", UpdatedAt: time.Date(2021, time.June, 5, 5, 15, 0, 0, time.Local)}},
|
||||
{ObjHeader: types.ObjHeader{Id: "6", UpdatedAt: time.Date(2021, time.June, 6, 6, 16, 0, 0, time.Local)}},
|
||||
{ObjHeader: types.ObjHeader{Id: "7", UpdatedAt: time.Date(2021, time.June, 7, 7, 17, 0, 0, time.Local)}},
|
||||
{ObjHeader: types.ObjHeader{Id: "8", UpdatedAt: time.Date(2021, time.June, 8, 8, 18, 0, 0, time.Local)}},
|
||||
{ObjHeader: types.ObjHeader{Id: "9", UpdatedAt: time.Date(2021, time.June, 9, 9, 19, 0, 0, time.Local)}},
|
||||
{ObjHeader: types.ObjHeader{Id: "10", UpdatedAt: time.Date(2021, time.June, 10, 10, 20, 0, 0, time.Local)}},
|
||||
}
|
||||
|
||||
// TouchedAt is either greater or equal to UpdatedAt.
|
||||
testData[0].SetTouchedAt(time.Date(2021, time.June, 1, 1, 11, 0, 0, time.Local)) // 1
|
||||
testData[1].SetTouchedAt(time.Date(2021, time.June, 4, 4, 12, 0, 0, time.Local)) // 3
|
||||
testData[2].SetTouchedAt(time.Date(2021, time.June, 4, 2, 13, 0, 0, time.Local)) // 2
|
||||
testData[3].SetTouchedAt(time.Date(2021, time.June, 4, 4, 14, 0, 0, time.Local)) // 4
|
||||
testData[4].SetTouchedAt(time.Date(2021, time.June, 7, 5, 15, 0, 0, time.Local)) // 6
|
||||
testData[5].SetTouchedAt(time.Date(2021, time.June, 6, 6, 16, 0, 0, time.Local)) // 5
|
||||
testData[6].SetTouchedAt(time.Date(2021, time.June, 7, 7, 17, 0, 0, time.Local)) // 7
|
||||
testData[7].SetTouchedAt(time.Date(2021, time.June, 9, 8, 18, 0, 0, time.Local)) // 8
|
||||
testData[8].SetTouchedAt(time.Date(2021, time.June, 10, 11, 19, 0, 0, time.Local)) // 10
|
||||
testData[9].SetTouchedAt(time.Date(2021, time.June, 10, 10, 20, 0, 0, time.Local)) // 9
|
||||
|
||||
return testData
|
||||
}
|
||||
|
||||
func TestSelectEarliestUpdatedSubs(t *testing.T) {
|
||||
getOrder := func(subs []types.Subscription) string {
|
||||
var order []string
|
||||
for i := range subs {
|
||||
order = append(order, subs[i].Id)
|
||||
}
|
||||
return strings.Join(order, ",")
|
||||
}
|
||||
|
||||
subs := SelectEarliestUpdatedSubs(genTestData(), nil, 100)
|
||||
|
||||
// No sorting when returning the full set.
|
||||
expectedOrder := "1,2,3,4,5,6,7,8,9,10"
|
||||
sortOrder := getOrder(subs)
|
||||
if sortOrder != expectedOrder {
|
||||
t.Error("Wrong results returned. Expected:", expectedOrder, "; Got:", sortOrder)
|
||||
}
|
||||
|
||||
// Sorted, oldest 9 results.
|
||||
subs = SelectEarliestUpdatedSubs(genTestData(), nil, 9)
|
||||
expectedOrder = "1,3,2,4,6,5,7,8,10"
|
||||
sortOrder = getOrder(subs)
|
||||
if sortOrder != expectedOrder {
|
||||
t.Error("Limited query returned wrong results. Expected:", expectedOrder, "; Got:", sortOrder)
|
||||
}
|
||||
|
||||
// Sorted, oldest 9 results.
|
||||
subs = SelectEarliestUpdatedSubs(genTestData(), &types.QueryOpt{Limit: 20}, 9)
|
||||
expectedOrder = "1,3,2,4,6,5,7,8,10"
|
||||
sortOrder = getOrder(subs)
|
||||
if sortOrder != expectedOrder {
|
||||
t.Error("Limited query (2) returned wrong results. Expected:", expectedOrder, "; Got:", sortOrder)
|
||||
}
|
||||
|
||||
// Sorted, oldest 9 results.
|
||||
subs = SelectEarliestUpdatedSubs(genTestData(), &types.QueryOpt{Limit: 9}, 20)
|
||||
expectedOrder = "1,3,2,4,6,5,7,8,10"
|
||||
sortOrder = getOrder(subs)
|
||||
if sortOrder != expectedOrder {
|
||||
t.Error("Limited query (3) returned wrong results. Expected:", expectedOrder, "; Got:", sortOrder)
|
||||
}
|
||||
|
||||
ims := time.Date(2021, time.June, 7, 8, 16, 15, 0, time.Local)
|
||||
subs = SelectEarliestUpdatedSubs(genTestData(), &types.QueryOpt{Limit: 6, IfModifiedSince: &ims}, 20)
|
||||
expectedOrder = "8,10,9"
|
||||
sortOrder = getOrder(subs)
|
||||
if sortOrder != expectedOrder {
|
||||
t.Error("Date & count limited query returned wrong results. Expected:", expectedOrder, "; Got:", sortOrder)
|
||||
}
|
||||
|
||||
ims = time.Date(2021, time.June, 4, 4, 13, 15, 0, time.Local)
|
||||
subs = SelectEarliestUpdatedSubs(genTestData(), &types.QueryOpt{Limit: 3, IfModifiedSince: &ims}, 20)
|
||||
expectedOrder = "4,6,5"
|
||||
sortOrder = getOrder(subs)
|
||||
if sortOrder != expectedOrder {
|
||||
t.Error("Count & date limited query returned wrong results. Expected:", expectedOrder, "; Got:", sortOrder)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectLatestTime(t *testing.T) {
|
||||
t1 := time.Date(2021, time.June, 1, 10, 0, 0, 0, time.UTC)
|
||||
t2 := time.Date(2021, time.June, 2, 10, 0, 0, 0, time.UTC)
|
||||
|
||||
// t1 is before t2, should return t2
|
||||
result := SelectLatestTime(t1, t2)
|
||||
if !result.Equal(t2) {
|
||||
t.Errorf("Expected %v, got %v", t2, result)
|
||||
}
|
||||
|
||||
// t2 is after t1, should return t2
|
||||
result = SelectLatestTime(t2, t1)
|
||||
if !result.Equal(t2) {
|
||||
t.Errorf("Expected %v, got %v", t2, result)
|
||||
}
|
||||
|
||||
// Equal times, should return either one (in this case t1)
|
||||
result = SelectLatestTime(t1, t1)
|
||||
if !result.Equal(t1) {
|
||||
t.Errorf("Expected %v, got %v", t1, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRangesToSql(t *testing.T) {
|
||||
// Test single range with Hi = 0 (IN clause)
|
||||
ranges := []types.Range{{Low: 5, Hi: 0}}
|
||||
sql, args := RangesToSql(ranges)
|
||||
expectedSql := "IN (?)"
|
||||
expectedArgs := []any{5}
|
||||
if sql != expectedSql {
|
||||
t.Errorf("Expected SQL '%s', got '%s'", expectedSql, sql)
|
||||
}
|
||||
if !reflect.DeepEqual(args, expectedArgs) {
|
||||
t.Errorf("Expected args %v, got %v", expectedArgs, args)
|
||||
}
|
||||
|
||||
// Test single range with Hi > 0 (BETWEEN clause)
|
||||
ranges = []types.Range{{Low: 5, Hi: 8}}
|
||||
sql, args = RangesToSql(ranges)
|
||||
expectedSql = "BETWEEN ? AND ?"
|
||||
expectedArgs = []any{5, 7} // Hi-1 for BETWEEN
|
||||
if sql != expectedSql {
|
||||
t.Errorf("Expected SQL '%s', got '%s'", expectedSql, sql)
|
||||
}
|
||||
if !reflect.DeepEqual(args, expectedArgs) {
|
||||
t.Errorf("Expected args %v, got %v", expectedArgs, args)
|
||||
}
|
||||
|
||||
// Test multiple ranges (IN clause)
|
||||
ranges = []types.Range{{Low: 1, Hi: 3}, {Low: 5, Hi: 0}, {Low: 8, Hi: 10}}
|
||||
sql, args = RangesToSql(ranges)
|
||||
expectedSql = "IN (?,?,?,?,?)"
|
||||
expectedArgs = []any{1, 2, 5, 8, 9}
|
||||
if sql != expectedSql {
|
||||
t.Errorf("Expected SQL '%s', got '%s'", expectedSql, sql)
|
||||
}
|
||||
if !reflect.DeepEqual(args, expectedArgs) {
|
||||
t.Errorf("Expected args %v, got %v", expectedArgs, args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisjunctionSql(t *testing.T) {
|
||||
// Test single disjunction
|
||||
req := [][]string{{"tag1", "tag2", "tag3"}}
|
||||
sql, args := DisjunctionSql(req, "tagname")
|
||||
expectedSql := "HAVING COUNT(tagname IN (?,?,?) OR NULL)>=1 "
|
||||
expectedArgs := []any{"tag1", "tag2", "tag3"}
|
||||
if sql != expectedSql {
|
||||
t.Errorf("Expected SQL '%s', got '%s'", expectedSql, sql)
|
||||
}
|
||||
if !reflect.DeepEqual(args, expectedArgs) {
|
||||
t.Errorf("Expected args %v, got %v", expectedArgs, args)
|
||||
}
|
||||
|
||||
// Test multiple disjunctions
|
||||
req = [][]string{{"tag1", "tag2"}, {"tag3"}, {"tag4", "tag5"}}
|
||||
sql, args = DisjunctionSql(req, "fieldname")
|
||||
expectedSql = "HAVING COUNT(fieldname IN (?,?) OR NULL)>=1 AND COUNT(fieldname IN (?) OR NULL)>=1 AND COUNT(fieldname IN (?,?) OR NULL)>=1 "
|
||||
expectedArgs = []any{"tag1", "tag2", "tag3", "tag4", "tag5"}
|
||||
if sql != expectedSql {
|
||||
t.Errorf("Expected SQL '%s', got '%s'", expectedSql, sql)
|
||||
}
|
||||
if !reflect.DeepEqual(args, expectedArgs) {
|
||||
t.Errorf("Expected args %v, got %v", expectedArgs, args)
|
||||
}
|
||||
|
||||
// Test empty disjunctions (should be skipped)
|
||||
req = [][]string{{"tag1"}, {}, {"tag2"}}
|
||||
sql, args = DisjunctionSql(req, "fieldname")
|
||||
expectedSql = "HAVING COUNT(fieldname IN (?) OR NULL)>=1 AND COUNT(fieldname IN (?) OR NULL)>=1 "
|
||||
expectedArgs = []any{"tag1", "tag2"}
|
||||
if sql != expectedSql {
|
||||
t.Errorf("Expected SQL '%s', got '%s'", expectedSql, sql)
|
||||
}
|
||||
if !reflect.DeepEqual(args, expectedArgs) {
|
||||
t.Errorf("Expected args %v, got %v", expectedArgs, args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterFoundTags(t *testing.T) {
|
||||
setTags := types.StringSlice{"tag1", "tag2", "tag3", "tag4", "tag5"}
|
||||
index := map[string]struct{}{
|
||||
"tag1": {},
|
||||
"tag3": {},
|
||||
"tag5": {},
|
||||
"tag6": {}, // Not in setTags
|
||||
}
|
||||
|
||||
result := FilterFoundTags(setTags, index)
|
||||
expected := []string{"tag1", "tag3", "tag5"}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("Expected %v, got %v", expected, result)
|
||||
}
|
||||
|
||||
// Test with empty index
|
||||
emptyIndex := map[string]struct{}{}
|
||||
result = FilterFoundTags(setTags, emptyIndex)
|
||||
expected = []string{}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("Expected %v, got %v", expected, result)
|
||||
}
|
||||
|
||||
// Test with empty setTags
|
||||
emptyTags := types.StringSlice{}
|
||||
result = FilterFoundTags(emptyTags, index)
|
||||
expected = []string{}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("Expected %v, got %v", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToJSON(t *testing.T) {
|
||||
// Test with nil
|
||||
result := ToJSON(nil)
|
||||
if result != nil {
|
||||
t.Errorf("Expected nil, got %v", result)
|
||||
}
|
||||
|
||||
// Test with string
|
||||
input := "test string"
|
||||
result = ToJSON(input)
|
||||
expected := []byte(`"test string"`)
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("Expected %v, got %v", expected, result)
|
||||
}
|
||||
|
||||
// Test with map
|
||||
input2 := map[string]any{"key": "value", "number": 42}
|
||||
result = ToJSON(input2)
|
||||
// Parse back to verify
|
||||
var parsed map[string]any
|
||||
if err := json.Unmarshal(result, &parsed); err != nil {
|
||||
t.Errorf("Failed to unmarshal result: %v", err)
|
||||
}
|
||||
if parsed["key"] != "value" || parsed["number"] != float64(42) {
|
||||
t.Errorf("JSON conversion failed, got %v", parsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromJSON(t *testing.T) {
|
||||
// Test with nil
|
||||
result := FromJSON(nil)
|
||||
if result != nil {
|
||||
t.Errorf("Expected nil, got %v", result)
|
||||
}
|
||||
|
||||
// Test with valid JSON bytes
|
||||
input := []byte(`{"key": "value", "number": 42}`)
|
||||
result = FromJSON(input)
|
||||
if resultMap, ok := result.(map[string]any); ok {
|
||||
if resultMap["key"] != "value" || resultMap["number"] != float64(42) {
|
||||
t.Errorf("JSON deserialization failed, got %v", resultMap)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("Expected map[string]any, got %T", result)
|
||||
}
|
||||
|
||||
// Test with invalid JSON bytes
|
||||
invalidInput := []byte(`{invalid json}`)
|
||||
result = FromJSON(invalidInput)
|
||||
if result != nil {
|
||||
t.Errorf("Expected nil for invalid JSON, got %v", result)
|
||||
}
|
||||
|
||||
// Test with non-byte slice
|
||||
stringInput := "not bytes"
|
||||
result = FromJSON(stringInput)
|
||||
if result != nil {
|
||||
t.Errorf("Expected nil for non-byte input, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateByMap(t *testing.T) {
|
||||
update := map[string]any{
|
||||
"Name": "John Doe",
|
||||
"Age": 30,
|
||||
"Public": map[string]string{"avatar": "url"},
|
||||
"Private": map[string]string{"email": "john@example.com"},
|
||||
"Trusted": map[string]bool{"verified": true},
|
||||
"UpdatedAt": time.Now(),
|
||||
}
|
||||
|
||||
cols, args := UpdateByMap(update)
|
||||
|
||||
// Check that we have the right number of columns and args
|
||||
if len(cols) != len(args) || len(cols) != len(update) {
|
||||
t.Errorf("Expected %d columns and args, got %d cols and %d args", len(update), len(cols), len(args))
|
||||
}
|
||||
|
||||
// Verify column format
|
||||
for _, col := range cols {
|
||||
if !strings.Contains(col, "=?") {
|
||||
t.Errorf("Column should contain '=?', got %s", col)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that JSON fields are properly handled
|
||||
foundPublic := false
|
||||
foundPrivate := false
|
||||
foundTrusted := false
|
||||
for i, col := range cols {
|
||||
if strings.HasPrefix(col, "public=?") {
|
||||
foundPublic = true
|
||||
// Should be JSON bytes
|
||||
if _, ok := args[i].([]byte); !ok {
|
||||
t.Errorf("Public field should be []byte, got %T", args[i])
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(col, "private=?") {
|
||||
foundPrivate = true
|
||||
if _, ok := args[i].([]byte); !ok {
|
||||
t.Errorf("Private field should be []byte, got %T", args[i])
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(col, "trusted=?") {
|
||||
foundTrusted = true
|
||||
if _, ok := args[i].([]byte); !ok {
|
||||
t.Errorf("Trusted field should be []byte, got %T", args[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !foundPublic || !foundPrivate || !foundTrusted {
|
||||
t.Error("Missing JSON fields in output")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractTags(t *testing.T) {
|
||||
// Test with Tags field present
|
||||
update := map[string]any{
|
||||
"Name": "John",
|
||||
"Tags": types.StringSlice{"tag1", "tag2", "tag3"},
|
||||
"Age": 30,
|
||||
}
|
||||
tags := ExtractTags(update)
|
||||
expected := []string{"tag1", "tag2", "tag3"}
|
||||
if !reflect.DeepEqual(tags, expected) {
|
||||
t.Errorf("Expected %v, got %v", expected, tags)
|
||||
}
|
||||
|
||||
// Test with no Tags field
|
||||
update = map[string]any{
|
||||
"Name": "John",
|
||||
"Age": 30,
|
||||
}
|
||||
tags = ExtractTags(update)
|
||||
expected = nil
|
||||
if !reflect.DeepEqual(tags, expected) {
|
||||
t.Errorf("Expected %+v, got %+v", expected, tags)
|
||||
}
|
||||
|
||||
// Test with nil Tags field
|
||||
update = map[string]any{
|
||||
"Name": "John",
|
||||
"Tags": nil,
|
||||
"Age": 30,
|
||||
}
|
||||
tags = ExtractTags(update)
|
||||
expected = nil
|
||||
if !reflect.DeepEqual(tags, expected) {
|
||||
t.Errorf("Expected %+v, got %+v", expected, tags)
|
||||
}
|
||||
|
||||
// Test with wrong type for Tags field
|
||||
update = map[string]any{
|
||||
"Name": "John",
|
||||
"Tags": "not a slice",
|
||||
"Age": 30,
|
||||
}
|
||||
tags = ExtractTags(update)
|
||||
expected = nil
|
||||
if !reflect.DeepEqual(tags, expected) {
|
||||
t.Errorf("Expected %+v, got %+v", expected, tags)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
package test_data
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/tinode/chat/server/auth"
|
||||
"github.com/tinode/chat/server/db/common"
|
||||
"github.com/tinode/chat/server/store/types"
|
||||
)
|
||||
|
||||
type TestData struct {
|
||||
UGen *types.UidGenerator
|
||||
Users []*types.User
|
||||
Creds []*types.Credential
|
||||
Recs []common.AuthRecord
|
||||
Topics []*types.Topic
|
||||
Subs []*types.Subscription
|
||||
Msgs []*types.Message
|
||||
Devs []*types.DeviceDef
|
||||
Files []*types.FileDef
|
||||
// Tags: add, remove, reset
|
||||
Tags [][]string
|
||||
Now time.Time
|
||||
}
|
||||
|
||||
func initUsers(now time.Time) []*types.User {
|
||||
users := make([]*types.User, 0, 3)
|
||||
users = append(users, &types.User{
|
||||
ObjHeader: types.ObjHeader{
|
||||
Id: "3ysxkod5hNM",
|
||||
},
|
||||
UserAgent: "SomeAgent v1.2.3",
|
||||
Tags: []string{"alice"},
|
||||
})
|
||||
users = append(users, &types.User{
|
||||
ObjHeader: types.ObjHeader{
|
||||
Id: "9AVDamaNCRY",
|
||||
},
|
||||
UserAgent: "Tinode Web v111.222.333",
|
||||
Tags: []string{"bob"},
|
||||
})
|
||||
users = append(users, &types.User{
|
||||
ObjHeader: types.ObjHeader{
|
||||
Id: "0QLrX3WPS2o",
|
||||
},
|
||||
UserAgent: "Tindroid v1.2.3",
|
||||
Tags: []string{"carol"},
|
||||
})
|
||||
for _, user := range users {
|
||||
// Initialize timestamps.
|
||||
user.InitTimes()
|
||||
// Assign user.id from user.Id.
|
||||
user.Uid()
|
||||
}
|
||||
deletedAt := now.Add(10 * time.Minute)
|
||||
users[2].State = types.StateDeleted
|
||||
users[2].StateAt = &deletedAt
|
||||
return users
|
||||
}
|
||||
|
||||
func initCreds(now time.Time, users []*types.User) []*types.Credential {
|
||||
creds := make([]*types.Credential, 0, 6)
|
||||
creds = append(creds, &types.Credential{ // 0
|
||||
User: users[0].Id,
|
||||
Method: "email",
|
||||
Value: "alice@test.example.com",
|
||||
Done: true,
|
||||
})
|
||||
creds = append(creds, &types.Credential{ // 1
|
||||
User: users[1].Id,
|
||||
Method: "email",
|
||||
Value: "bob@test.example.com",
|
||||
Done: true,
|
||||
})
|
||||
creds = append(creds, &types.Credential{ // 2
|
||||
User: users[1].Id,
|
||||
Method: "email",
|
||||
Value: "bob@test.example.com",
|
||||
})
|
||||
creds = append(creds, &types.Credential{ // 3
|
||||
User: users[2].Id,
|
||||
Method: "tel",
|
||||
Value: "+998991112233",
|
||||
})
|
||||
creds = append(creds, &types.Credential{ // 4
|
||||
User: users[2].Id,
|
||||
Method: "tel",
|
||||
Value: "+998993332211",
|
||||
Done: true,
|
||||
})
|
||||
creds = append(creds, &types.Credential{ // 5
|
||||
User: users[2].Id,
|
||||
Method: "email",
|
||||
Value: "asdf@example.com",
|
||||
})
|
||||
for _, cred := range creds {
|
||||
cred.InitTimes()
|
||||
}
|
||||
creds[3].CreatedAt = now.Add(-10 * time.Minute)
|
||||
creds[3].UpdatedAt = now.Add(-10 * time.Minute)
|
||||
return creds
|
||||
}
|
||||
|
||||
func initAuthRecords(now time.Time, users []*types.User) []common.AuthRecord {
|
||||
recs := make([]common.AuthRecord, 0, 2)
|
||||
recs = append(recs, common.AuthRecord{
|
||||
Unique: "basic:alice",
|
||||
UserId: users[0].Id,
|
||||
Scheme: "basic",
|
||||
AuthLvl: auth.LevelAuth,
|
||||
Secret: []byte{'a', 'l', 'i', 'c', 'e'},
|
||||
Expires: now.Add(24 * time.Hour),
|
||||
})
|
||||
recs = append(recs, common.AuthRecord{
|
||||
Unique: "basic:bob",
|
||||
UserId: users[1].Id,
|
||||
Scheme: "basic",
|
||||
AuthLvl: auth.LevelAuth,
|
||||
Secret: []byte{'b', 'o', 'b'},
|
||||
Expires: now.Add(24 * time.Hour),
|
||||
})
|
||||
return recs
|
||||
}
|
||||
|
||||
func initTopics(now time.Time, users []*types.User) []*types.Topic {
|
||||
topics := make([]*types.Topic, 0, 5)
|
||||
topics = append(topics, &types.Topic{
|
||||
ObjHeader: types.ObjHeader{
|
||||
Id: "grpgRXf0rU4uR4",
|
||||
CreatedAt: now.Add(10 * time.Minute),
|
||||
UpdatedAt: now,
|
||||
},
|
||||
TouchedAt: now,
|
||||
Owner: users[0].Id,
|
||||
SeqId: 111,
|
||||
Tags: []string{"travel", "zxcv"},
|
||||
SubCnt: 2,
|
||||
})
|
||||
topics = append(topics, &types.Topic{
|
||||
ObjHeader: types.ObjHeader{
|
||||
Id: "p2p9AVDamaNCRbfKzGSh3mE0w",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
TouchedAt: now,
|
||||
SeqId: 12,
|
||||
})
|
||||
topics = append(topics, &types.Topic{
|
||||
ObjHeader: types.ObjHeader{
|
||||
Id: "p2pxQLrX3WPS2rfKzGSh3mE0w",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
TouchedAt: now,
|
||||
SeqId: 15,
|
||||
})
|
||||
topics = append(topics, &types.Topic{
|
||||
ObjHeader: types.ObjHeader{
|
||||
Id: "p2pE1iE7I9JN5ESv44HiLbj1A",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
TouchedAt: now,
|
||||
SeqId: 555,
|
||||
})
|
||||
topics = append(topics, &types.Topic{
|
||||
ObjHeader: types.ObjHeader{
|
||||
Id: "p2pQvr1xwKU01LfKzGSh3mE0w",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
TouchedAt: now,
|
||||
SeqId: 333,
|
||||
})
|
||||
return topics
|
||||
}
|
||||
|
||||
func initSubs(now time.Time, users []*types.User, topics []*types.Topic) []*types.Subscription {
|
||||
subs := make([]*types.Subscription, 0, 6)
|
||||
subs = append(subs, &types.Subscription{
|
||||
ObjHeader: types.ObjHeader{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now.Add(10 * time.Minute),
|
||||
},
|
||||
User: users[0].Id,
|
||||
Topic: topics[0].Id,
|
||||
RecvSeqId: 5,
|
||||
ReadSeqId: 1,
|
||||
ModeWant: 255,
|
||||
ModeGiven: 255,
|
||||
})
|
||||
subs = append(subs, &types.Subscription{
|
||||
ObjHeader: types.ObjHeader{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now.Add(15 * time.Minute),
|
||||
},
|
||||
User: users[1].Id,
|
||||
Topic: topics[0].Id,
|
||||
RecvSeqId: 6,
|
||||
ReadSeqId: 3,
|
||||
ModeWant: 47,
|
||||
ModeGiven: 47,
|
||||
})
|
||||
subs = append(subs, &types.Subscription{
|
||||
ObjHeader: types.ObjHeader{
|
||||
CreatedAt: now.Add(-10 * time.Hour),
|
||||
UpdatedAt: now.Add(-10 * time.Hour),
|
||||
},
|
||||
User: users[0].Id,
|
||||
Topic: topics[1].Id,
|
||||
RecvSeqId: 9,
|
||||
ReadSeqId: 5,
|
||||
ModeWant: 47,
|
||||
ModeGiven: 47,
|
||||
})
|
||||
subs = append(subs, &types.Subscription{
|
||||
ObjHeader: types.ObjHeader{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now.Add(20 * time.Minute),
|
||||
},
|
||||
User: users[1].Id,
|
||||
Topic: topics[1].Id,
|
||||
RecvSeqId: 9,
|
||||
ReadSeqId: 5,
|
||||
ModeWant: 47,
|
||||
ModeGiven: 47,
|
||||
})
|
||||
subs = append(subs, &types.Subscription{
|
||||
ObjHeader: types.ObjHeader{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now.Add(30 * time.Minute),
|
||||
},
|
||||
User: users[2].Id,
|
||||
Topic: topics[2].Id,
|
||||
RecvSeqId: 0,
|
||||
ReadSeqId: 0,
|
||||
ModeWant: 47,
|
||||
ModeGiven: 47,
|
||||
})
|
||||
subs = append(subs, &types.Subscription{
|
||||
ObjHeader: types.ObjHeader{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now.Add(40 * time.Minute),
|
||||
},
|
||||
User: users[2].Id,
|
||||
Topic: topics[3].Id,
|
||||
RecvSeqId: 555,
|
||||
ReadSeqId: 455,
|
||||
ModeWant: 47,
|
||||
ModeGiven: 47,
|
||||
})
|
||||
for _, sub := range subs {
|
||||
sub.SetTouchedAt(now)
|
||||
}
|
||||
return subs
|
||||
}
|
||||
|
||||
func initMessages(users []*types.User, topics []*types.Topic) []*types.Message {
|
||||
msgs := make([]*types.Message, 0, 6)
|
||||
msgs = append(msgs, &types.Message{
|
||||
SeqId: 1,
|
||||
Topic: topics[0].Id,
|
||||
From: users[0].Id,
|
||||
Content: "msg1",
|
||||
})
|
||||
msgs = append(msgs, &types.Message{
|
||||
SeqId: 2,
|
||||
Topic: topics[0].Id,
|
||||
From: users[2].Id,
|
||||
Content: "msg2",
|
||||
DeletedFor: []types.SoftDelete{{
|
||||
User: users[0].Id,
|
||||
DelId: 1}},
|
||||
})
|
||||
msgs = append(msgs, &types.Message{
|
||||
SeqId: 3,
|
||||
Topic: topics[0].Id,
|
||||
From: users[0].Id,
|
||||
Content: "msg31",
|
||||
})
|
||||
msgs = append(msgs, &types.Message{
|
||||
SeqId: 1,
|
||||
Topic: topics[1].Id,
|
||||
From: users[1].Id,
|
||||
Content: "msg1",
|
||||
})
|
||||
msgs = append(msgs, &types.Message{
|
||||
SeqId: 5,
|
||||
Topic: topics[1].Id,
|
||||
From: users[1].Id,
|
||||
Content: "msg2",
|
||||
})
|
||||
msgs = append(msgs, &types.Message{
|
||||
SeqId: 11,
|
||||
Topic: topics[1].Id,
|
||||
From: users[0].Id,
|
||||
Content: "msg3",
|
||||
})
|
||||
|
||||
for i, msg := range msgs {
|
||||
msg.InitTimes()
|
||||
msg.SetUid(types.Uid(i + 1))
|
||||
}
|
||||
return msgs
|
||||
}
|
||||
|
||||
func initDevices(now time.Time) []*types.DeviceDef {
|
||||
devs := make([]*types.DeviceDef, 0, 2)
|
||||
devs = append(devs, &types.DeviceDef{
|
||||
DeviceId: "2934ujfoviwj09ntf094",
|
||||
Platform: "Android",
|
||||
LastSeen: now,
|
||||
Lang: "en_EN",
|
||||
})
|
||||
devs = append(devs, &types.DeviceDef{
|
||||
DeviceId: "pogpjb023b09gfdmp",
|
||||
Platform: "iOS",
|
||||
LastSeen: now,
|
||||
Lang: "en_EN",
|
||||
})
|
||||
return devs
|
||||
}
|
||||
|
||||
func initFileDefs(now time.Time, users []*types.User) []*types.FileDef {
|
||||
files := make([]*types.FileDef, 0, 2)
|
||||
files = append(files, &types.FileDef{
|
||||
ObjHeader: types.ObjHeader{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
Status: types.UploadStarted,
|
||||
User: users[0].Id,
|
||||
MimeType: "application/pdf",
|
||||
Location: "uploads/qwerty.pdf",
|
||||
Size: 123456,
|
||||
})
|
||||
files = append(files, &types.FileDef{
|
||||
ObjHeader: types.ObjHeader{
|
||||
CreatedAt: now.Add(60 * time.Minute),
|
||||
UpdatedAt: now.Add(60 * time.Minute),
|
||||
},
|
||||
Status: types.UploadStarted,
|
||||
User: users[0].Id,
|
||||
Location: "uploads/asdf.txt",
|
||||
Size: 654321,
|
||||
})
|
||||
files[0].SetUid(types.Uid(1001))
|
||||
files[1].SetUid(types.Uid(1002))
|
||||
return files
|
||||
}
|
||||
|
||||
func initTags() [][]string {
|
||||
// Tags must be lowercase and non-repeating.
|
||||
addTags := []string{"tag1", "alice"}
|
||||
removeTags := []string{"alice", "tag1", "tag2"}
|
||||
resetTags := []string{"alice", "tag111", "tag333"}
|
||||
return [][]string{addTags, removeTags, resetTags}
|
||||
}
|
||||
|
||||
func InitTestData() *TestData {
|
||||
// Use fixed timestamp to make tests more predictable
|
||||
var now = time.Date(2021, time.June, 12, 11, 39, 24, 15, time.Local).UTC().Round(time.Millisecond)
|
||||
var uGen = &types.UidGenerator{}
|
||||
if err := uGen.Init(11, []byte("testtesttesttest")); err != nil {
|
||||
return nil
|
||||
}
|
||||
var users = initUsers(now)
|
||||
var topics = initTopics(now, users)
|
||||
return &TestData{
|
||||
UGen: uGen,
|
||||
Users: users,
|
||||
Creds: initCreds(now, users),
|
||||
Recs: initAuthRecords(now, users),
|
||||
Topics: topics,
|
||||
Subs: initSubs(now, users, topics),
|
||||
Msgs: initMessages(users, topics),
|
||||
Devs: initDevices(now),
|
||||
Files: initFileDefs(now, users),
|
||||
Tags: initTags(),
|
||||
Now: now,
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,323 @@
|
||||
# MongoDB Database Schema
|
||||
|
||||
## Database `tinode`
|
||||
|
||||
### Table `users`
|
||||
Stores user accounts
|
||||
|
||||
Fields:
|
||||
* `_id` user id, primary key
|
||||
* `createdat` timestamp when the user was created
|
||||
* `updatedat` timestamp when user metadata was updated
|
||||
* `access` user's default access level for peer-to-peer topics
|
||||
* `auth`, `anon` default permissions for authenticated and anonymous users
|
||||
* `public` application-defined data
|
||||
* `state` account state: normal (ok), suspended, soft-deleted
|
||||
* `stateat` timestamp when the state was last updated or NULL
|
||||
* `lastseen` timestamp when the user was last online
|
||||
* `useragent` client User-Agent used when last online
|
||||
* `tags` unique strings for user discovery
|
||||
* `devices` client devices for push notifications
|
||||
* `deviceid` device registration ID
|
||||
* `platform` device platform string (iOS, Android, Web)
|
||||
* `lastseen` last logged in
|
||||
* `lang` device language, ISO code
|
||||
|
||||
Indexes:
|
||||
* `_id` primary key
|
||||
* `tags` multikey-index (indexed array)
|
||||
* `deletedat` index
|
||||
* `deviceids` multikey-index of push notification tokens
|
||||
|
||||
Sample:
|
||||
```json
|
||||
{
|
||||
"access": {
|
||||
"anon": 0 ,
|
||||
"auth": 47
|
||||
} ,
|
||||
"createdat": "2019-10-11T12:13:14.522Z" ,
|
||||
"state": 0,
|
||||
"stateat": null ,
|
||||
"devices": null ,
|
||||
"_id": "7yUCHniegrM" ,
|
||||
"lastseen": "2019-10-11T12:13:14.522Z" ,
|
||||
"public": {
|
||||
"fn": "Alice Johnson" ,
|
||||
"photo": {
|
||||
"data": Binary('/9j/4AAQSkZJRgAB...'),
|
||||
"type": "jpg"
|
||||
}
|
||||
} ,
|
||||
"state": 1 ,
|
||||
"tags": [
|
||||
"email:alice@example.com" ,
|
||||
"tel:17025550001"
|
||||
] ,
|
||||
"updatedat": "2019-10-11T12:13:14.522Z",
|
||||
"useragent": "TinodeWeb/0.13 (MacIntel) tinodejs/0.13"
|
||||
}
|
||||
```
|
||||
|
||||
### Table `auth`
|
||||
Stores authentication secrets
|
||||
|
||||
Fields:
|
||||
* `_id` unique string which identifies this record, primary key; defined as "_authentication scheme_':'_some unique value per scheme_"
|
||||
* `userid` ID of the user who owns the record
|
||||
* `secret` shared secret, for instance bcrypt of password
|
||||
* `authLvl` authentication level
|
||||
* `expires` timestamp when the records expires
|
||||
|
||||
|
||||
Indexes:
|
||||
* `_id` primary key
|
||||
* `userid` index
|
||||
|
||||
Sample:
|
||||
```json
|
||||
{
|
||||
"_id": "basic:alice" ,
|
||||
"authLvl": 20 ,
|
||||
"expires": "2019-10-11T12:13:14.522Z" ,
|
||||
"secret": Binary('/9j/RgAB...'),
|
||||
"userid": "7yUCHniegrM"
|
||||
}
|
||||
```
|
||||
|
||||
### Table `topics`
|
||||
The table stores topics.
|
||||
|
||||
Fields:
|
||||
* `_id` name of the topic, primary key
|
||||
* `createdat` topic creation time
|
||||
* `updatedat` timestamp of the last change to topic metadata
|
||||
* `access` stores topic's default access permissions
|
||||
* `auth`, `anon` permissions for authenticated and anonymous users respectively
|
||||
* `owner` ID of the user who owns the topic
|
||||
* `public` application-defined data
|
||||
* `state` topic state: normal (ok), suspended, soft-deleted
|
||||
* `stateat` timestamp when the state was last updated or NULL
|
||||
* `seqid` sequential ID of the last message
|
||||
* `delid` topic-sequential ID of the deletion operation
|
||||
* `usebt` currently unused
|
||||
|
||||
Indexes:
|
||||
* `_id` primary key
|
||||
* `owner` index
|
||||
* `tags` multikey index
|
||||
|
||||
Sample:
|
||||
```json
|
||||
{
|
||||
"access": {
|
||||
"anon": 64 ,
|
||||
"auth": 64
|
||||
} ,
|
||||
"delid": 0,
|
||||
"createdat": "2019-10-11T12:13:14.522Z",
|
||||
"lastmessageat": "2019-10-11T12:13:14.522Z" ,
|
||||
"id": "p2pavVGHLCBbKrvJQIeeJ6Csw" ,
|
||||
"owner": "v2JyG4OLSoA" ,
|
||||
"public": {
|
||||
"fn": "Travel, travel, travel" ,
|
||||
"photo": {
|
||||
"data": Binary('/9j/RgAB...') ,
|
||||
"type": "jpg"
|
||||
}
|
||||
} ,
|
||||
"seqid": 14,
|
||||
"state": 0,
|
||||
"stateat": null,
|
||||
"updatedat": "2019-10-11T12:13:14.522Z" ,
|
||||
"usebt": false
|
||||
}
|
||||
```
|
||||
|
||||
### Table `subscriptions`
|
||||
The table stores relationships between users and topics.
|
||||
|
||||
Fields:
|
||||
* `_id` used for object retrieval
|
||||
* `createdat` timestamp when the user was created
|
||||
* `updatedat` timestamp when user metadata was updated
|
||||
* `deletedat` currently unused
|
||||
* `readseqid` id of the message last read by the user
|
||||
* `recvseqid` id of the message last received by user device
|
||||
* `delid` topic-sequential ID of the soft-deletion operation
|
||||
* `topic` name of the topic subscribed to
|
||||
* `user` subscriber's user ID
|
||||
* `modewant` access mode that user wants when accessing the topic
|
||||
* `modegiven` access mode granted to user by the topic
|
||||
* `private` application-defined data, accessible by the user only
|
||||
|
||||
Indexes:
|
||||
* `_id` primary key composed as "_topic name_':'_user ID_"
|
||||
* `user` index
|
||||
* `topic` index
|
||||
|
||||
Sample:
|
||||
```json
|
||||
{
|
||||
"_id": "grpjajVKrHn0PU:v2JyG4OLSoA" ,
|
||||
"createdat": "2019-10-11T12:13:14.522Z" ,
|
||||
"updatedat": "2019-10-11T12:13:14.522Z" ,
|
||||
"deletedat": null ,
|
||||
"user": "v2JyG4OLSoA",
|
||||
"topic": "grpjajVKrHn0PU" ,
|
||||
"recvseqid": 0 ,
|
||||
"readseqid": 0 ,
|
||||
"modewant": 47 ,
|
||||
"modegiven": 47 ,
|
||||
"private": "Kirgudu" ,
|
||||
"state": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Table `messages`
|
||||
The table stores `{data}` messages
|
||||
|
||||
Fields:
|
||||
* `_id` currently unused, primary key
|
||||
* `createdat` timestamp when the message was created
|
||||
* `updatedat` initially equal to CreatedAt, for deleted messages equal to DeletedAt
|
||||
* `deletedfor` array of user IDs which soft-deleted the message
|
||||
* `delid` topic-sequential ID of the soft-deletion operation
|
||||
* `user` ID of the user who soft-deleted the message
|
||||
* `from` ID of the user who generated this message
|
||||
* `topic` which received this message
|
||||
* `seqid` messages ID - sequential number of the message in the topic
|
||||
* `head` message headers
|
||||
* `attachments` denormalized IDs of files attached to the message
|
||||
* `content` application-defined message payload
|
||||
|
||||
Indexes:
|
||||
* `_id` primary key
|
||||
|
||||
Sample:
|
||||
```json
|
||||
{
|
||||
"_id": "LLXKEe9W4Bs" ,
|
||||
"createdat": "2019-10-11T12:13:14.522Z" ,
|
||||
"updatedat": "2019-10-11T12:13:14.522Z",
|
||||
"deletedfor": [
|
||||
{
|
||||
"delid": 1 ,
|
||||
"user": "wTI0jO9rEqY"
|
||||
}
|
||||
] ,
|
||||
"seqid": 3 ,
|
||||
"topic": "p2pJhbJnya8z5PBMjSM72sSpg",
|
||||
"from": "wTI0jO9rEqY" ,
|
||||
"head": {
|
||||
"mime": "text/x-drafty"
|
||||
} ,
|
||||
"content": {
|
||||
"fmt": [
|
||||
{
|
||||
"len": 6 ,
|
||||
"tp": "ST"
|
||||
}
|
||||
] ,
|
||||
"txt": "Hello!"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Table `dellog`
|
||||
The table stores records of message deletions
|
||||
|
||||
Fields:
|
||||
* `_id` currently unused, primary key
|
||||
* `createdat` timestamp when the record was created
|
||||
* `updatedat` timestamp equal to CreatedAt
|
||||
* `delid` topic-sequential ID of the deletion operation.
|
||||
* `deletedfor` ID of the user for soft-deletions, blank string for hard-deletions
|
||||
* `topic` affected topic
|
||||
* `seqidranges` array of ranges of deleted message IDs (see `messages.seqid`)
|
||||
|
||||
Indexes:
|
||||
* `_id` primary key
|
||||
* `topic_delid` compound index `["Topic", "DelId"]`
|
||||
|
||||
Sample:
|
||||
```json
|
||||
{
|
||||
"_id": "9LfrjW349Rc",
|
||||
"createdat": "2019-10-11T12:13:14.522Z",
|
||||
"updatedat": "2019-10-11T12:13:14.522Z",
|
||||
"topic": "grpGx7fpjQwVC0",
|
||||
"delid": 18,
|
||||
"deletedfor": "xY-YHx09-WI",
|
||||
"seqidranges": [
|
||||
{
|
||||
"low": 20,
|
||||
"hi": 25
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Table `credentials`
|
||||
The tables stores user credentials used for validation.
|
||||
|
||||
* `_id` credential, primary key
|
||||
* `createdat` timestamp when the record was created
|
||||
* `updatedat` timestamp when the last validation attempt was performed (successful or not).
|
||||
* `method` validation method
|
||||
* `done` indicator if the credential is validated
|
||||
* `resp` expected validation response
|
||||
* `retries` number of failed attempts at validation
|
||||
* `user` id of the user who owns this credential
|
||||
* `value` value of the credential
|
||||
|
||||
Indexes:
|
||||
* `_id` Primary key composed either as `user`:`method`:`value` for unconfirmed credentials or as `method`:`value` for confirmed.
|
||||
* `user` Index
|
||||
|
||||
Sample:
|
||||
```json
|
||||
{
|
||||
"Id": "tel:+17025550001",
|
||||
"CreatedAt": "2019-10-11T12:13:14.522Z",
|
||||
"UpdatedAt": "2019-10-11T12:13:14.522Z",
|
||||
"Method": "tel" ,
|
||||
"Done": true ,
|
||||
"Resp": "123456" ,
|
||||
"Retries": 0 ,
|
||||
"User": "k3srBRk9RYw" ,
|
||||
"Value": "+17025550001"
|
||||
}
|
||||
```
|
||||
|
||||
### Table `fileuploads`
|
||||
The table stores records of uploaded files. The files themselves are stored outside of the database.
|
||||
* `_id` unique user-visible file name, primary key
|
||||
* `createdat` timestamp when the record was created
|
||||
* `updatedat` timestamp of when th upload has cmpleted or failed
|
||||
* `user` id of the user who uploaded this file.
|
||||
* `location` actual location of the file on the server.
|
||||
* `mimetype` file content type as a [Mime](https://en.wikipedia.org/wiki/MIME) string.
|
||||
* `size` size of the file in bytes. Could be 0 if upload has not completed yet.
|
||||
* `usecount` count of messages referencing this file.
|
||||
* `status` upload status: 0 pending, 1 completed, -1 failed.
|
||||
|
||||
Indexes:
|
||||
* `_id` file name, primary key
|
||||
* `user` index
|
||||
* `usecount` index
|
||||
|
||||
Sample:
|
||||
```json
|
||||
{
|
||||
"_id": "sFmjlQ_kA6A" ,
|
||||
"createdat": "2019-10-11T12:13:14.522Z" ,
|
||||
"updatedat": "2019-10-11T12:13:14.522Z" ,
|
||||
"location": "uploads/sFmjlQ_kA6A" ,
|
||||
"mimetype": "image/jpeg" ,
|
||||
"size": 54961090 ,
|
||||
"usecount": 3,
|
||||
"status": 1 ,
|
||||
"user": "7j-RR1V7O3Y"
|
||||
}
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"reset_db_data": true,
|
||||
"adapters": {
|
||||
"mongodb": {
|
||||
"database": "tinode_test",
|
||||
//"replica_set": "rs0",
|
||||
"addresses": "localhost:27017",
|
||||
//"username": "tinode_test",
|
||||
//"password": "tinode_test",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,271 @@
|
||||
# THIS SCHEMA FILE IS FOR REFERENCE/DOCUMENTATION ONLY!
|
||||
# DO NOT USE IT TO INITIALIZE THE DATABASE.
|
||||
# Read installation instructions first.
|
||||
|
||||
# The following line will produce an intentional error.
|
||||
|
||||
'READ INSTALLATION INSTRUCTIONS!';
|
||||
|
||||
# The actual schema is below.
|
||||
|
||||
DROP DATABASE IF EXISTS tinode;
|
||||
|
||||
CREATE DATABASE tinode CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
||||
|
||||
USE tinode;
|
||||
|
||||
CREATE TABLE im_tenant(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
code VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
tenant_desc VARCHAR(256),
|
||||
state SMALLINT NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_by BIGINT NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT NOT NULL,
|
||||
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_tenant_code(code)
|
||||
);
|
||||
|
||||
CREATE TABLE im_tenant_config(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
max_users BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
max_groups BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
max_group_members INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_by BIGINT NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT NOT NULL,
|
||||
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_tenant_config_tenant_id(tenant_id),
|
||||
CONSTRAINT fk_im_tenant_config_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO im_tenant(code,name,tenant_desc,state,created_by,updated_by)
|
||||
VALUES('default','Default Tenant',NULL,1,0,0);
|
||||
|
||||
|
||||
CREATE TABLE kvmeta(
|
||||
`key` VARCHAR(64),
|
||||
createdat DATETIME(3),
|
||||
`value` TEXT,
|
||||
PRIMARY KEY(`key`),
|
||||
INDEX kvmeta_createdat_key(createdat, `key`)
|
||||
);
|
||||
|
||||
INSERT INTO kvmeta(`key`, `value`) VALUES("version", "117");
|
||||
|
||||
CREATE TABLE users(
|
||||
id BIGINT NOT NULL,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
state SMALLINT NOT NULL DEFAULT 0,
|
||||
stateat DATETIME(3),
|
||||
access JSON,
|
||||
lastseen DATETIME,
|
||||
useragent VARCHAR(255) DEFAULT '',
|
||||
public JSON,
|
||||
tags JSON, -- Denormalized array of tags
|
||||
|
||||
PRIMARY KEY(id),
|
||||
INDEX users_state_stateat(state, stateat),
|
||||
INDEX users_lastseen_updatedat(lastseen, updatedat)
|
||||
);
|
||||
|
||||
# Indexed user tags.
|
||||
CREATE TABLE usertags(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
userid BIGINT NOT NULL,
|
||||
tag VARCHAR(96) NOT NULL,
|
||||
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY(userid) REFERENCES users(id),
|
||||
INDEX usertags_tag(tag),
|
||||
UNIQUE INDEX usertags_userid_tag(userid, tag)
|
||||
);
|
||||
|
||||
# Indexed devices. Normalized into a separate table.
|
||||
CREATE TABLE devices(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
userid BIGINT NOT NULL,
|
||||
hash CHAR(16) NOT NULL,
|
||||
deviceid TEXT NOT NULL,
|
||||
platform VARCHAR(32),
|
||||
lastseen DATETIME NOT NULL,
|
||||
lang VARCHAR(8),
|
||||
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY(userid) REFERENCES users(id),
|
||||
UNIQUE INDEX devices_hash(hash)
|
||||
);
|
||||
|
||||
# Authentication records for the basic authentication scheme.
|
||||
CREATE TABLE auth(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
uname VARCHAR(32) NOT NULL,
|
||||
userid BIGINT NOT NULL,
|
||||
scheme VARCHAR(16) NOT NULL,
|
||||
authlvl SMALLINT NOT NULL,
|
||||
secret VARCHAR(255) NOT NULL,
|
||||
expires DATETIME,
|
||||
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY(userid) REFERENCES users(id),
|
||||
UNIQUE INDEX auth_userid_scheme(userid, scheme),
|
||||
UNIQUE INDEX auth_uname (uname)
|
||||
);
|
||||
|
||||
|
||||
# Topics
|
||||
CREATE TABLE topics(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
touchedat DATETIME(3),
|
||||
state SMALLINT NOT NULL DEFAULT 0,
|
||||
stateat DATETIME(3),
|
||||
name CHAR(25) NOT NULL,
|
||||
usebt TINYINT DEFAULT 0,
|
||||
owner BIGINT NOT NULL DEFAULT 0,
|
||||
access JSON,
|
||||
seqid INT NOT NULL DEFAULT 0,
|
||||
delid INT DEFAULT 0,
|
||||
subcnt INT DEFAULT 0,
|
||||
public JSON,
|
||||
trusted JSON,
|
||||
tags JSON, -- Denormalized array of tags
|
||||
aux JSON,
|
||||
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX topics_name (name),
|
||||
INDEX topics_owner(owner),
|
||||
INDEX topics_state_stateat(state, stateat),
|
||||
INDEX topics_name_state_seqid ON topics(name, state, seqid)
|
||||
);
|
||||
|
||||
# Indexed topic tags.
|
||||
CREATE TABLE topictags(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
topic CHAR(25) NOT NULL,
|
||||
tag VARCHAR(96) NOT NULL,
|
||||
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY(topic) REFERENCES topics(name),
|
||||
INDEX topictags_tag (tag),
|
||||
UNIQUE INDEX topictags_topic_tag(topic, tag)
|
||||
);
|
||||
|
||||
# Subscriptions
|
||||
CREATE TABLE subscriptions(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
deletedat DATETIME(3),
|
||||
userid BIGINT NOT NULL,
|
||||
topic CHAR(25) NOT NULL,
|
||||
delid INT DEFAULT 0,
|
||||
recvseqid INT DEFAULT 0,
|
||||
readseqid INT DEFAULT 0,
|
||||
modewant CHAR(8),
|
||||
modegiven CHAR(8),
|
||||
private JSON,
|
||||
|
||||
PRIMARY KEY(id) ,
|
||||
FOREIGN KEY(userid) REFERENCES users(id),
|
||||
UNIQUE INDEX subscriptions_topic_userid(topic, userid),
|
||||
INDEX subscriptions_topic(topic),
|
||||
INDEX subscriptions_deletedat(deletedat),
|
||||
INDEX subscriptions_user_topic_deletedat ON subscriptions(userid, topic, deletedat)
|
||||
);
|
||||
|
||||
# Messages
|
||||
CREATE TABLE messages(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
deletedat DATETIME(3),
|
||||
delid INT DEFAULT 0,
|
||||
seqid INT NOT NULL,
|
||||
topic CHAR(25) NOT NULL,
|
||||
`from` BIGINT NOT NULL,
|
||||
head JSON,
|
||||
content JSON,
|
||||
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY(topic) REFERENCES topics(name),
|
||||
UNIQUE INDEX messages_topic_seqid (topic, seqid)
|
||||
);
|
||||
|
||||
# Deletion log
|
||||
CREATE TABLE dellog(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
topic CHAR(25) NOT NULL,
|
||||
deletedfor BIGINT NOT NULL DEFAULT 0,
|
||||
delid INT NOT NULL,
|
||||
low INT NOT NULL,
|
||||
hi INT NOT NULL,
|
||||
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY(topic) REFERENCES topics(name),
|
||||
# For getting the list of deleted message ranges
|
||||
INDEX dellog_topic_delid_deletedfor(topic,delid,deletedfor),
|
||||
# Used when getting not-yet-deleted messages(messages LEFT JOIN dellog)
|
||||
INDEX dellog_topic_deletedfor_low_hi(topic,deletedfor,low,hi),
|
||||
# Used when deleting a user
|
||||
INDEX dellog_deletedfor(deletedfor)
|
||||
);
|
||||
|
||||
# User credentials
|
||||
CREATE TABLE credentials(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
deletedat DATETIME(3),
|
||||
method VARCHAR(16) NOT NULL,
|
||||
value VARCHAR(128) NOT NULL,
|
||||
synthetic VARCHAR(192) NOT NULL,
|
||||
userid BIGINT NOT NULL,
|
||||
resp VARCHAR(255) NOT NULL,
|
||||
done TINYINT NOT NULL DEFAULT 0,
|
||||
retries INT NOT NULL DEFAULT 0,
|
||||
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE credentials_uniqueness(synthetic),
|
||||
FOREIGN KEY(userid) REFERENCES users(id),
|
||||
);
|
||||
|
||||
# Records of uploaded files. Files themselves are stored elsewhere.
|
||||
CREATE TABLE fileuploads(
|
||||
id BIGINT NOT NULL,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
userid BIGINT,
|
||||
status INT NOT NULL,
|
||||
mimetype VARCHAR(255) NOT NULL,
|
||||
size BIGINT NOT NULL,
|
||||
location VARCHAR(2048) NOT NULL,
|
||||
etag VARCHAR(128),
|
||||
|
||||
PRIMARY KEY(id),
|
||||
INDEX fileuploads_status(status)
|
||||
);
|
||||
|
||||
# Links between uploaded files and messages or topics.
|
||||
CREATE TABLE filemsglinks(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
fileid BIGINT NOT NULL,
|
||||
msgid INT,
|
||||
topic CHAR(25),
|
||||
userid BIGINT,
|
||||
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY(fileid) REFERENCES fileuploads(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(msgid) REFERENCES messages(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(topicid) REFERENCES topics(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(userid) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"reset_db_data": true,
|
||||
"adapters": {
|
||||
"mysql": {
|
||||
"dsn": "root@tcp(localhost:3306)/tinode_test?parseTime=true&collation=utf8mb4_unicode_ci",
|
||||
// Name of the main database.
|
||||
"database": "tinode_test"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
# The MySQL and PostrgreSQL schemas are identical save for differences in SQL flavors.
|
||||
# SEE ../mysql/schema.sql.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"reset_db_data": true,
|
||||
"adapters": {
|
||||
"postgres": {
|
||||
"User": "postgres",
|
||||
"Passwd": "postgres",
|
||||
"Host": "localhost",
|
||||
"Port": "5432",
|
||||
"DBName": "tinode_test"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,331 @@
|
||||
# RethinkDB Database Schema
|
||||
|
||||
## Database `tinode`
|
||||
|
||||
### Table `users`
|
||||
Stores user accounts
|
||||
|
||||
Fields:
|
||||
* `Id` user id, primary key
|
||||
* `CreatedAt` timestamp when the user was created
|
||||
* `UpdatedAt` timestamp when user metadata was updated
|
||||
* `State` account state: normal (ok), suspended, soft-deleted
|
||||
* `StateAt` timestamp when the state was last updated or NULL
|
||||
* `Access` user's default access level for peer-to-peer topics
|
||||
* `Auth`, `Anon` default permissions for authenticated and anonymous users
|
||||
* `Public` application-defined data
|
||||
* `State` state of the user: normal, disabled, deleted
|
||||
* `StateAt` timestamp when the state was last updated or NULL
|
||||
* `LastSeen` timestamp when the user was last online
|
||||
* `UserAgent` client User-Agent used when last online
|
||||
* `Tags` unique strings for user discovery
|
||||
* `Devices` client devices for push notifications
|
||||
* `DeviceId` device registration ID
|
||||
* `Platform` device platform string (iOS, Android, Web)
|
||||
* `LastSeen` last logged in
|
||||
* `Lang` device language, ISO code
|
||||
|
||||
Indexes:
|
||||
* `Id` primary key
|
||||
* `Tags` multi-index (indexed array)
|
||||
* `DeletedAt` index
|
||||
* `DeviceIds` multi-index of push notification tokens
|
||||
|
||||
Sample:
|
||||
```js
|
||||
{
|
||||
"Access": {
|
||||
"Anon": 0 ,
|
||||
"Auth": 47
|
||||
} ,
|
||||
"CreatedAt": Mon Jul 24 2017 11:16:38 GMT+00:00 ,
|
||||
"State": 0,
|
||||
"StateAt": null ,
|
||||
"Devices": null ,
|
||||
"Id": "7yUCHniegrM" ,
|
||||
"LastSeen": Mon Jan 01 1 00:00:00 GMT+00:00 ,
|
||||
"Public": {
|
||||
"fn": "Alice Johnson" ,
|
||||
"photo": {
|
||||
"data": <binary, 6.5KB, "ff d8 ff e0 00 10..."> ,
|
||||
"type": "jpg"
|
||||
}
|
||||
} ,
|
||||
"State": 1 ,
|
||||
"Tags": [
|
||||
"email:alice@example.com" ,
|
||||
"tel:17025550001"
|
||||
] ,
|
||||
"UpdatedAt": Mon Jul 24 2017 11:16:38 GMT+00:00 ,
|
||||
"UserAgent": "TinodeWeb/0.13 (MacIntel) tinodejs/0.13"
|
||||
}
|
||||
```
|
||||
|
||||
### Table `auth`
|
||||
Stores authentication secrets
|
||||
|
||||
Fields:
|
||||
* `userid` ID of the user who owns the record
|
||||
* `unique` unique string which identifies this record, primary key; defined as "_authentication scheme_':'_some unique value per scheme_"
|
||||
* `secret` shared secret, for instance bcrypt of password
|
||||
* `authLvl` authentication level
|
||||
* `expires` timestamp when the records expires
|
||||
|
||||
|
||||
Indexes:
|
||||
* `unique` primary key
|
||||
* `userid` index
|
||||
|
||||
Sample:
|
||||
```js
|
||||
{
|
||||
"authLvl": 20 ,
|
||||
"expires": Mon Jan 01 1 00:00:00 GMT+00:00 ,
|
||||
"secret": <binary, 60 bytes, "24 32 61 24 31 30..."> ,
|
||||
"unique": "basic:alice" ,
|
||||
"userid": "7yUCHniegrM"
|
||||
}
|
||||
```
|
||||
|
||||
### Table `topics`
|
||||
The table stores topics.
|
||||
|
||||
Fields:
|
||||
* `Id` name of the topic, primary key
|
||||
* `CreatedAt` topic creation time
|
||||
* `UpdatedAt` timestamp of the last change to topic metadata
|
||||
* `State` topic state: normal (ok), suspended, soft-deleted
|
||||
* `StateAt` timestamp when the state was last updated or NULL
|
||||
* `Access` stores topic's default access permissions
|
||||
* `Auth`, `Anon` permissions for authenticated and anonymous users respectively
|
||||
* `Owner` ID of the user who owns the topic
|
||||
* `Public` application-defined data
|
||||
* `State` state of the topic: normal, disabled, deleted
|
||||
* `SeqId` sequential ID of the last message
|
||||
* `DelId` topic-sequential ID of the deletion operation
|
||||
* `UseBt` indicator that channel functionality is enabled in the topic
|
||||
|
||||
Indexes:
|
||||
* `Id` primary key
|
||||
* `Owner` index
|
||||
|
||||
Sample:
|
||||
```js
|
||||
{
|
||||
"Access": {
|
||||
"Anon": 64 ,
|
||||
"Auth": 64
|
||||
} ,
|
||||
"DelId": 0,
|
||||
"CreatedAt": Thu Oct 15 2015 04:06:51 GMT+00:00 ,
|
||||
"State": 0 ,
|
||||
"StateAt": null ,
|
||||
"LastMessageAt": Sat Oct 17 2015 13:51:56 GMT+00:00 ,
|
||||
"Id": "p2pavVGHLCBbKrvJQIeeJ6Csw" ,
|
||||
"Owner": "v2JyG4OLSoA" ,
|
||||
"Public": {
|
||||
"fn": "Travel, travel, travel" ,
|
||||
"photo": {
|
||||
"data": <binary, 6.2KB, "ff d8 ff e0 00 10..."> ,
|
||||
"type": "jpg"
|
||||
}
|
||||
} ,
|
||||
"SeqId": 14,
|
||||
"State": 0 ,
|
||||
"UpdatedAt": Thu Oct 15 2015 04:06:51 GMT+00:00 ,
|
||||
"UseBt": false
|
||||
}
|
||||
```
|
||||
|
||||
### Table `subscriptions`
|
||||
The table stores relationships between users and topics.
|
||||
|
||||
Fields:
|
||||
* `Id` used for object retrieval
|
||||
* `CreatedAt` timestamp when the subscription was created
|
||||
* `UpdatedAt` timestamp when the subscription was updated
|
||||
* `DeletedAt` timestamp when the subscription was deleted
|
||||
* `ReadSeqId` id of the message last read by the user
|
||||
* `RecvSeqId` id of the message last received by any user device
|
||||
* `DelId` topic-sequential ID of the soft-deletion operation
|
||||
* `Topic` name of the topic subscribed to
|
||||
* `User` subscriber's user ID
|
||||
* `ModeWant` access mode that user wants when accessing the topic
|
||||
* `ModeGiven` access mode granted to user by the topic
|
||||
* `Private` application-defined data, accessible by the user only
|
||||
|
||||
Indexes:
|
||||
* `Id` primary key composed as "_topic name_':'_user ID_"
|
||||
* `User` index
|
||||
* `Topic` index
|
||||
|
||||
Sample:
|
||||
```js
|
||||
{
|
||||
"ClearId": 0 ,
|
||||
"CreatedAt": Tue Jul 25 2017 15:34:39 GMT+00:00 ,
|
||||
"DeletedAt": null ,
|
||||
"Id": "grpjajVKrHn0PU:v2JyG4OLSoA" ,
|
||||
"ModeGiven": 47 ,
|
||||
"ModeWant": 47 ,
|
||||
"Private": "Kirgudu" ,
|
||||
"ReadSeqId": 0 ,
|
||||
"RecvSeqId": 0 ,
|
||||
"State": 0 ,
|
||||
"Topic": "grpjajVKrHn0PU" ,
|
||||
"UpdatedAt": Tue Jul 25 2017 15:34:39 GMT+00:00 ,
|
||||
"User": "v2JyG4OLSoA"
|
||||
}
|
||||
```
|
||||
|
||||
### Table `messages`
|
||||
The table stores `{data}` messages
|
||||
|
||||
Fields:
|
||||
* `Id` currently unused, primary key
|
||||
* `CreatedAt` timestamp when the message was created
|
||||
* `UpdatedAt` initially equal to CreatedAt, for deleted messages equal to DeletedAt
|
||||
* `DeletedFor` array of user IDs which soft-deleted the message
|
||||
* `DelId` topic-sequential ID of the soft-deletion operation
|
||||
* `User` ID of the user who soft-deleted the message
|
||||
* `From` ID of the user who generated this message
|
||||
* `Topic` which received this message
|
||||
* `SeqId` messages ID - sequential number of the message in the topic
|
||||
* `Head` message headers
|
||||
* `Attachments` denormalized IDs of files attached to the message
|
||||
* `Content` application-defined message payload
|
||||
|
||||
Indexes:
|
||||
* `Id` primary key
|
||||
* `Topic_SeqId` compound index `["Topic", "SeqId"]`
|
||||
* `Topic_DelId` compound index `["Topic", "DelId"]`
|
||||
* `Topic_DeletedFor` compound multi-index `["Topic", "DeletedFor"("User"), "DeletedFor"("DelId")]`
|
||||
|
||||
Sample:
|
||||
```js
|
||||
{
|
||||
"Content": {
|
||||
"fmt": [
|
||||
{
|
||||
"len": 6 ,
|
||||
"tp": "ST"
|
||||
}
|
||||
] ,
|
||||
"txt": "Hello!"
|
||||
} ,
|
||||
"CreatedAt": Sun Dec 24 2017 05:16:23 GMT+00:00 ,
|
||||
"From": "wTI0jO9rEqY" ,
|
||||
"Head": {
|
||||
"mime": "text/x-drafty"
|
||||
} ,
|
||||
"DeletedFor": [
|
||||
{
|
||||
"DelId": 1 ,
|
||||
"User": "wTI0jO9rEqY"
|
||||
}
|
||||
] ,
|
||||
"Id": "LLXKEe9W4Bs" ,
|
||||
"SeqId": 3 ,
|
||||
"Topic": "p2pJhbJnya8z5PBMjSM72sSpg" ,
|
||||
"UpdatedAt": Sun Dec 24 2017 05:16:23 GMT+00:00
|
||||
}
|
||||
```
|
||||
|
||||
### Table `dellog`
|
||||
The table stores records of message deletions
|
||||
|
||||
Fields:
|
||||
* `Id` currently unused, primary key
|
||||
* `CreatedAt` timestamp when the record was created
|
||||
* `UpdatedAt` timestamp equal to CreatedAt
|
||||
* `DelId` topic-sequential ID of the deletion operation.
|
||||
* `DeletedFor` ID of the user for soft-deletions, blank string for hard-deletions
|
||||
* `Topic` affected topic
|
||||
* `SeqIdRanges` array of ranges of deleted message IDs (see `messages.SeqId`)
|
||||
|
||||
Indexes:
|
||||
* `Id` primary key
|
||||
* `Topic_DelId` compound index `["Topic", "DelId"]`
|
||||
|
||||
Sample:
|
||||
```js
|
||||
{
|
||||
"Id": "9LfrjW349Rc",
|
||||
"CreatedAt": Tue Dec 05 2017 01:51:38 GMT+00:00,
|
||||
"DelId": 18,
|
||||
"DeletedFor": "xY-YHx09-WI" ,
|
||||
|
||||
"SeqIdRanges": [
|
||||
{
|
||||
"Low": 20,
|
||||
"Hi": 25,
|
||||
}
|
||||
] ,
|
||||
"Topic": "grpGx7fpjQwVC0" ,
|
||||
"UpdatedAt": Tue Dec 05 2017 01:51:38 GMT+00:00
|
||||
}
|
||||
```
|
||||
|
||||
### Table `credentials`
|
||||
The tables stores user credentials used for validation.
|
||||
|
||||
* `Id` credential, primary key
|
||||
* `CreatedAt` timestamp when the record was created
|
||||
* `UpdatedAt` timestamp when the last validation attempt was performed (successful or not).
|
||||
* `Method` validation method
|
||||
* `Done` indicator if the credential is validated
|
||||
* `Resp` expected validation response
|
||||
* `Retries` number of failed attempts at validation
|
||||
* `User` id of the user who owns this credential
|
||||
* `Value` value of the credential
|
||||
* `Closed` unvalidated credential is no longer being validated. Only one credential is not Closed for each user/method.
|
||||
|
||||
Indexes:
|
||||
* `Id` Primary key composed either as `User`:`Method`:`Value` for unconfirmed credentials or as `Method`:`Value` for confirmed.
|
||||
* `User` Index
|
||||
|
||||
Sample:
|
||||
```js
|
||||
{
|
||||
"Id": "tel:+17025550001",
|
||||
"CreatedAt": Sun Jun 10 2018 16:37:27 GMT+00:00 ,
|
||||
"UpdatedAt": Sun Jun 10 2018 16:37:28 GMT+00:00 ,
|
||||
"Method": "tel" ,
|
||||
"Done": true ,
|
||||
"Resp": "123456" ,
|
||||
"Retries": 0 ,
|
||||
"User": "k3srBRk9RYw" ,
|
||||
"Value": "+17025550001"
|
||||
}
|
||||
```
|
||||
|
||||
### Table `fileuploads`
|
||||
The table stores records of uploaded files. The files themselves are stored outside of the database.
|
||||
* `Id` unique user-visible file name, primary key
|
||||
* `CreatedAt` timestamp when the record was created
|
||||
* `UpdatedAt` timestamp of when th upload has cmpleted or failed
|
||||
* `User` id of the user who uploaded this file.
|
||||
* `Location` actual location of the file on the server.
|
||||
* `MimeType` file content type as a [Mime](https://en.wikipedia.org/wiki/MIME) string.
|
||||
* `Size` size of the file in bytes. Could be 0 if upload has not completed yet.
|
||||
* `UseCount` count of messages referencing this file.
|
||||
* `Status` upload status: 0 pending, 1 completed, -1 failed.
|
||||
|
||||
Indexes:
|
||||
* `Id` primary key
|
||||
* `UseCount` index
|
||||
|
||||
Sample:
|
||||
```js
|
||||
{
|
||||
"CreatedAt": Sun Jun 10 2018 16:38:45 GMT+00:00 ,
|
||||
"Id": "sFmjlQ_kA6A" ,
|
||||
"Location": "uploads/sFmjlQ_kA6A" ,
|
||||
"MimeType": "image/jpeg" ,
|
||||
"Size": 54961090 ,
|
||||
"UseCount": 3,
|
||||
"Status": 1,
|
||||
"UpdatedAt": Sun Jun 10 2018 16:38:45 GMT+00:00 ,
|
||||
"User": "7j-RR1V7O3Y"
|
||||
}
|
||||
```
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"reset_db_data": true,
|
||||
"adapters": {
|
||||
"rethinkdb": {
|
||||
// Address(es) of RethinkDB node(s): either a string or an array of strings.
|
||||
"addresses": "localhost:28015",
|
||||
// Name of the main database.
|
||||
"database": "tinode_test"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user