From bccb74717193cb4f4722242876a8cfaae33d9d2f Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Fri, 2 Oct 2026 17:38:48 +0200 Subject: [PATCH] feat: add the calendar feed model and migration --- models/calendar_feed.go | 72 +++++++++++++++++++ registry.gen.go | 2 + ...61002153237_create_jz_bm_calendar_feeds.go | 32 +++++++++ 3 files changed, 106 insertions(+) create mode 100644 models/calendar_feed.go create mode 100644 updates/20261002153237_create_jz_bm_calendar_feeds.go diff --git a/models/calendar_feed.go b/models/calendar_feed.go new file mode 100644 index 0000000..54871af --- /dev/null +++ b/models/calendar_feed.go @@ -0,0 +1,72 @@ +// Code generated by summer make. DO NOT EDIT. + +package models + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "time" +) + +// CalendarFeed is a student's subscribable ICS calendar: a secret token +// that makes the URL private, and the student's choice of what the feed +// lists. One feed per user (a golem15.user users row). +type CalendarFeed struct { + ID uint `gorm:"column:id;primaryKey"` + UserID uint `gorm:"column:user_id"` + // Token is 32 random bytes in hex. The feed is read-only and low in + // sensitivity, so the token is stored as it is. + Token string `gorm:"column:token"` + // ExcludedCourseIDs are the courses the feed leaves out, such as the + // elective alternatives the student did not choose. + ExcludedCourseIDs IDList `gorm:"column:excluded_course_ids"` + IncludeMeetings bool `gorm:"column:include_meetings"` + IncludeDeadlines bool `gorm:"column:include_deadlines"` + LastFetchedAt *time.Time `gorm:"column:last_fetched_at"` + CreatedAt time.Time `gorm:"column:created_at"` + UpdatedAt time.Time `gorm:"column:updated_at"` +} + +// TableName is the plugin table. +func (CalendarFeed) TableName() string { return "jz_bm_calendar_feeds" } + +// IDList is a JSONB array of IDs. A nil list is stored as []. +type IDList []uint + +// Value stores the list as a JSON array. +func (l IDList) Value() (driver.Value, error) { + if l == nil { + return "[]", nil + } + b, err := json.Marshal([]uint(l)) + if err != nil { + return nil, err + } + return string(b), nil +} + +// Scan reads a JSON array; NULL is an empty list. +func (l *IDList) Scan(src any) error { + var raw []byte + switch v := src.(type) { + case nil: + *l = IDList{} + return nil + case string: + raw = []byte(v) + case []byte: + raw = v + default: + return fmt.Errorf("jz.bm: cannot scan %T into IDList", src) + } + var ids []uint + if err := json.Unmarshal(raw, &ids); err != nil { + return fmt.Errorf("jz.bm: IDList: %w", err) + } + if ids == nil { + ids = []uint{} + } + *l = ids + return nil +} diff --git a/registry.gen.go b/registry.gen.go index 794d376..631feff 100644 --- a/registry.gen.go +++ b/registry.gen.go @@ -13,6 +13,7 @@ import ( func generatedModels() []any { return []any{ + &models.CalendarFeed{}, &models.ClassSession{}, &models.Course{}, &models.Deadline{}, @@ -37,6 +38,7 @@ func generatedMigrations() []*gormigrate.Migration { updates.CreateMaterials(), updates.CreateMaterialTag(), updates.AddElectiveGroupToCourses(), + updates.CreateCalendarFeeds(), } } diff --git a/updates/20261002153237_create_jz_bm_calendar_feeds.go b/updates/20261002153237_create_jz_bm_calendar_feeds.go new file mode 100644 index 0000000..e2f0283 --- /dev/null +++ b/updates/20261002153237_create_jz_bm_calendar_feeds.go @@ -0,0 +1,32 @@ +// Code generated by summer make. DO NOT EDIT. + +package updates + +import ( + "github.com/go-gormigrate/gormigrate/v2" + "gorm.io/gorm" +) + +// CreateCalendarFeeds returns the 20261002153237_create_jz_bm_calendar_feeds gormigrate entry: +// one subscribable ICS feed per golem15.user user, addressed by a secret token. +func CreateCalendarFeeds() *gormigrate.Migration { + return &gormigrate.Migration{ + ID: "20261002153237_create_jz_bm_calendar_feeds", + Migrate: func(tx *gorm.DB) error { + return execAll(tx, `CREATE TABLE jz_bm_calendar_feeds ( + id BIGSERIAL PRIMARY KEY, + user_id INTEGER NOT NULL UNIQUE REFERENCES users (id) ON DELETE CASCADE, + token VARCHAR(64) NOT NULL UNIQUE, + excluded_course_ids JSONB NOT NULL DEFAULT '[]'::jsonb CHECK (jsonb_typeof(excluded_course_ids) = 'array'), + include_meetings BOOLEAN NOT NULL DEFAULT TRUE, + include_deadlines BOOLEAN NOT NULL DEFAULT TRUE, + last_fetched_at TIMESTAMPTZ NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +)`) + }, + Rollback: func(tx *gorm.DB) error { + return tx.Exec("DROP TABLE IF EXISTS jz_bm_calendar_feeds").Error + }, + } +}