notes

package
v0.4.3-dev.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 10, 2026 License: AGPL-3.0 Imports: 17 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrNoteNotFound = errors.New("note not found")
View Source
var ErrQuotaExceeded = errors.New("quota exceeded")

ErrQuotaExceeded is returned by UpsertNote when a free-plan user attempts to create a new note beyond their plan limit.

Functions

This section is empty.

Types

type ConflictError added in v0.2.0

type ConflictError struct {
	ServerNote *Note
}

ConflictError is returned when the server's stored version is newer than the client's base version. It carries the current server note so the handler can return it in the 409 body, allowing the client to resolve the conflict and re-push without an extra round trip.

func (*ConflictError) Error added in v0.2.0

func (e *ConflictError) Error() string

type Note

type Note struct {
	ID               uuid.UUID  `gorm:"column:note_id;primaryKey;type:uuid"`
	UserID           uuid.UUID  `gorm:"column:user_id;type:uuid;not null;index:idx_user_updated,priority:1"`
	User             users.User `gorm:"constraint:OnDelete:CASCADE;"`
	EncryptedPayload []byte     `gorm:"column:encrypted_payload;type:bytea;not null"`
	CreatedAt        time.Time  `gorm:"autoCreateTime"`
	UpdatedAt        time.Time  `gorm:"autoUpdateTime;index:idx_user_updated,priority:2"`
	TrashedAt        *time.Time `gorm:"column:trashed_at;index"`
}

type NoteCursor

type NoteCursor struct {
	AfterUpdatedAt time.Time `json:"updated_at"`
	AfterNoteID    uuid.UUID `json:"note_id"`
}

NoteCursor is the position marker for cursor-based pagination of notes, ordered by (updated_at ASC, note_id ASC).

type NoteResponse

type NoteResponse struct {
	NoteID           uuid.UUID  `json:"note_id"`
	EncryptedPayload string     `json:"encrypted_payload"`
	CreatedAt        *time.Time `json:"created_at"`
	UpdatedAt        *time.Time `json:"updated_at"`
	TrashedAt        *time.Time `json:"trashed_at"`
}

type NoteTombstone

type NoteTombstone struct {
	NoteID    uuid.UUID  `gorm:"column:note_id;primaryKey;type:uuid"`
	UserID    uuid.UUID  `gorm:"column:user_id;type:uuid;not null;index"`
	User      users.User `gorm:"constraint:OnDelete:CASCADE;"`
	DeletedAt time.Time  `gorm:"not null"`
}

type NotesHandler

type NotesHandler struct {
	// contains filtered or unexported fields
}

func NewNotesHandler

func NewNotesHandler(service service, tracker analytics.Tracker) *NotesHandler

func (*NotesHandler) GetAll

func (h *NotesHandler) GetAll(c *echo.Context) error

func (*NotesHandler) GetByID

func (h *NotesHandler) GetByID(c *echo.Context) error

func (*NotesHandler) Purge added in v0.2.0

func (h *NotesHandler) Purge(c *echo.Context) error

Purge hard-deletes a note and creates a tombstone for sync clients.

func (*NotesHandler) Restore added in v0.2.0

func (h *NotesHandler) Restore(c *echo.Context) error

Restore clears the trashed_at timestamp on a note and returns the updated note.

func (*NotesHandler) Trash added in v0.2.0

func (h *NotesHandler) Trash(c *echo.Context) error

Trash soft-deletes a note by setting its trashed_at timestamp.

func (*NotesHandler) Upsert

func (h *NotesHandler) Upsert(c *echo.Context) error

type NotesListResponse

type NotesListResponse struct {
	Notes      []NoteResponse      `json:"notes"`
	Tombstones []TombstoneResponse `json:"tombstones"`
	NextCursor *string             `json:"next_cursor"` // non-null when more pages exist
	ServerTime *time.Time          `json:"server_time"`
}

type NotesRepository

type NotesRepository struct {
	// contains filtered or unexported fields
}

func NewNotesRepository

func NewNotesRepository(db *gorm.DB) *NotesRepository

func (*NotesRepository) CountLiveNotes

func (r *NotesRepository) CountLiveNotes(ctx context.Context, userID uuid.UUID) (int64, error)

CountLiveNotes returns the number of non-trashed notes for a user. Trashed notes (trashed_at IS NOT NULL) are excluded from the count so that a trashed note does not consume quota.

func (*NotesRepository) FindAll

func (r *NotesRepository) FindAll(ctx context.Context, userID uuid.UUID) ([]Note, error)

func (*NotesRepository) FindAllPaginated

func (r *NotesRepository) FindAllPaginated(ctx context.Context, userID uuid.UUID, limit int, afterUpdatedAt time.Time, afterNoteID uuid.UUID) ([]Note, error)

FindAllPaginated returns up to limit notes for the user, ordered by (updated_at ASC, note_id ASC). Pass zero values for afterUpdatedAt and afterNoteID to start from the beginning.

func (*NotesRepository) FindAllTombstones

func (r *NotesRepository) FindAllTombstones(ctx context.Context, userID uuid.UUID) ([]NoteTombstone, error)

func (*NotesRepository) FindByID

func (r *NotesRepository) FindByID(ctx context.Context, noteID uuid.UUID, userID uuid.UUID) (*Note, error)

func (*NotesRepository) FindTombstonesSince

func (r *NotesRepository) FindTombstonesSince(ctx context.Context, userID uuid.UUID, since time.Time) ([]NoteTombstone, error)

func (*NotesRepository) FindTombstonesSincePaginated added in v0.2.0

func (r *NotesRepository) FindTombstonesSincePaginated(ctx context.Context, userID uuid.UUID, since time.Time, limit int, afterDeletedAt time.Time, afterNoteID uuid.UUID) ([]NoteTombstone, error)

FindTombstonesSincePaginated returns up to limit tombstones for the user whose deleted_at is strictly greater than since, ordered by (deleted_at ASC, note_id ASC). Pass zero values for afterDeletedAt and afterNoteID to start from the first page. Pass the last page's deleted_at and note_id to continue from a previous page.

func (*NotesRepository) FindUpdatedSince

func (r *NotesRepository) FindUpdatedSince(ctx context.Context, userID uuid.UUID, since time.Time) ([]Note, error)

func (*NotesRepository) FindUpdatedSincePaginated added in v0.2.0

func (r *NotesRepository) FindUpdatedSincePaginated(ctx context.Context, userID uuid.UUID, since time.Time, limit int, afterUpdatedAt time.Time, afterNoteID uuid.UUID) ([]Note, error)

FindUpdatedSincePaginated returns up to limit notes for the user whose updated_at is strictly greater than since, ordered by (updated_at ASC, note_id ASC). Pass zero values for afterUpdatedAt and afterNoteID to start from the first page. Pass the last page's updated_at and note_id to continue from a previous page.

func (*NotesRepository) Purge added in v0.2.0

func (r *NotesRepository) Purge(ctx context.Context, noteID uuid.UUID, userID uuid.UUID) error

Purge hard-deletes a note row and creates a NoteTombstone within a single transaction. Returns ErrNoteNotFound if no matching row exists for the given noteID and userID.

func (*NotesRepository) PurgeExpiredTombstones

func (r *NotesRepository) PurgeExpiredTombstones(ctx context.Context, olderThan time.Time) (int64, error)

func (*NotesRepository) Restore added in v0.2.0

func (r *NotesRepository) Restore(ctx context.Context, noteID uuid.UUID, userID uuid.UUID) error

Restore clears trashed_at on a note, making it live again. Returns ErrNoteNotFound if no matching row exists for the given noteID and userID.

func (*NotesRepository) Trash added in v0.2.0

func (r *NotesRepository) Trash(ctx context.Context, noteID uuid.UUID, userID uuid.UUID) error

Trash soft-deletes a note by setting trashed_at to the current time. Returns ErrNoteNotFound if no matching row exists for the given noteID and userID.

func (*NotesRepository) Upsert

func (r *NotesRepository) Upsert(ctx context.Context, note *Note) error

type NotesService

type NotesService struct {
	// contains filtered or unexported fields
}

NotesService implements business logic for note operations.

func NewNotesService

func NewNotesService(repository repository, usersRepo usersRepository, freeNoteLimit int) *NotesService

NewNotesService constructs a NotesService. usersRepo is used to look up user plans for quota enforcement. freeNoteLimit is the maximum number of live notes allowed for free-plan users.

func (*NotesService) GetNoteByID

func (s *NotesService) GetNoteByID(ctx context.Context, noteID uuid.UUID, userID uuid.UUID) (*Note, error)

func (*NotesService) GetNotesSince

func (s *NotesService) GetNotesSince(ctx context.Context, userID uuid.UUID, since *time.Time, cursor *NoteCursor, limit int) ([]Note, []NoteTombstone, bool, error)

GetNotesSince returns notes and tombstones for the user.

When since is non-nil (delta sync), notes updated after that time are returned paginated using cursor-based keyset pagination ordered by (updated_at ASC, note_id ASC). Tombstones are likewise paginated by (deleted_at ASC, note_id ASC). Pass a non-nil cursor to continue from a previous page. hasMore is true when there are additional pages to fetch.

When since is nil (full sync), notes are paginated using the same cursor-based mechanism but across all notes, and all tombstones are returned on the first page only.

func (*NotesService) PurgeNote added in v0.2.0

func (s *NotesService) PurgeNote(ctx context.Context, noteID uuid.UUID, userID uuid.UUID) error

PurgeNote hard-deletes a note and creates a tombstone for sync clients.

func (*NotesService) RestoreNote added in v0.2.0

func (s *NotesService) RestoreNote(ctx context.Context, noteID uuid.UUID, userID uuid.UUID) (*Note, error)

RestoreNote clears the trashed_at timestamp and returns the updated note.

func (*NotesService) TrashNote added in v0.2.0

func (s *NotesService) TrashNote(ctx context.Context, noteID uuid.UUID, userID uuid.UUID) error

TrashNote soft-deletes a note by setting its trashed_at timestamp.

func (*NotesService) UpsertNote

func (s *NotesService) UpsertNote(ctx context.Context, userID uuid.UUID, noteID uuid.UUID, encryptedPayload []byte, baseVersion *time.Time) (*Note, bool, error)

UpsertNote creates or updates a note for the given user. The returned bool is true when the note was newly created and false for any update (versioned update, conflict-free update, or re-push of an existing note).

When baseVersion is non-nil, the operation is treated as a versioned update: a conflict check is performed and quota is never enforced (the user is not adding a new note). When baseVersion is nil, the method first checks whether the note already exists:

  • If it does not exist, this is a new-note create — quota is enforced for free-plan users before the upsert is attempted.
  • If it exists, this is a re-push of an existing note — quota is not enforced and no conflict check is applied.

type TombstoneResponse

type TombstoneResponse struct {
	NoteID    uuid.UUID  `json:"note_id"`
	DeletedAt *time.Time `json:"deleted_at"`
}

type UpsertNoteRequest

type UpsertNoteRequest struct {
	EncryptedPayload string     `json:"encrypted_payload" validate:"required"`
	BaseVersion      *time.Time `json:"base_version"` // the updated_at the client last saw; omit on first write
}

type UserPlan added in v0.2.0

type UserPlan struct {
	Plan string
}

UserPlan holds the subset of user data needed for quota enforcement. It is a value type so that the notes package does not import the users package.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL