diff --git a/api/v1/redirector.go b/api/v1/redirector.go index bb46981..f929c7a 100644 --- a/api/v1/redirector.go +++ b/api/v1/redirector.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/url" + "strings" "github.com/boojack/slash/store" "github.com/labstack/echo/v4" @@ -42,11 +43,36 @@ func (s *APIV1Service) registerRedirectorRoutes(g *echo.Group) { return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to create activity, err: %s", err)).SetInternal(err) } - if isValidURLString(shortcut.Link) { + return redirectToShortcut(c, shortcut) + }) +} + +func redirectToShortcut(c echo.Context, shortcut *store.Shortcut) error { + isValidURL := isValidURLString(shortcut.Link) + if shortcut.OpenGraphMetadata == nil { + if isValidURL { return c.Redirect(http.StatusSeeOther, shortcut.Link) } return c.String(http.StatusOK, shortcut.Link) - }) + } + + htmlTemplate := `
%s%s` + metadataList := []string{ + fmt.Sprintf(``, shortcut.OpenGraphMetadata.Title), + fmt.Sprintf(``, shortcut.OpenGraphMetadata.Description), + fmt.Sprintf(``, shortcut.OpenGraphMetadata.Image), + } + if isValidURL { + metadataList = append(metadataList, fmt.Sprintf(``, shortcut.Link)) + } + body := "" + if isValidURL { + body = fmt.Sprintf(``, shortcut.Link) + } else { + body = shortcut.Link + } + htmlString := fmt.Sprintf(htmlTemplate, strings.Join(metadataList, ""), body) + return c.HTML(http.StatusOK, htmlString) } func (s *APIV1Service) createShortcutViewActivity(c echo.Context, shortcut *store.Shortcut) error { diff --git a/api/v1/shortcut.go b/api/v1/shortcut.go index 29d9e7a..f92c14d 100644 --- a/api/v1/shortcut.go +++ b/api/v1/shortcut.go @@ -30,6 +30,12 @@ func (v Visibility) String() string { return string(v) } +type OpenGraphMetadata struct { + Title string `json:"title"` + Description string `json:"description"` + Image string `json:"image"` +} + type Shortcut struct { ID int `json:"id"` @@ -41,29 +47,32 @@ type Shortcut struct { RowStatus RowStatus `json:"rowStatus"` // Domain specific fields - Name string `json:"name"` - Link string `json:"link"` - Description string `json:"description"` - Visibility Visibility `json:"visibility"` - Tags []string `json:"tags"` - View int `json:"view"` + Name string `json:"name"` + Link string `json:"link"` + Description string `json:"description"` + Visibility Visibility `json:"visibility"` + Tags []string `json:"tags"` + View int `json:"view"` + OpenGraphMetadata *OpenGraphMetadata `json:"openGraphMetadata"` } type CreateShortcutRequest struct { - Name string `json:"name"` - Link string `json:"link"` - Description string `json:"description"` - Visibility Visibility `json:"visibility"` - Tags []string `json:"tags"` + Name string `json:"name"` + Link string `json:"link"` + Description string `json:"description"` + Visibility Visibility `json:"visibility"` + Tags []string `json:"tags"` + OpenGraphMetadata *OpenGraphMetadata `json:"openGraphMetadata"` } type PatchShortcutRequest struct { - RowStatus *RowStatus `json:"rowStatus"` - Name *string `json:"name"` - Link *string `json:"link"` - Description *string `json:"description"` - Visibility *Visibility `json:"visibility"` - Tags []string `json:"tags"` + RowStatus *RowStatus `json:"rowStatus"` + Name *string `json:"name"` + Link *string `json:"link"` + Description *string `json:"description"` + Visibility *Visibility `json:"visibility"` + Tags []string `json:"tags"` + OpenGraphMetadata *OpenGraphMetadata `json:"openGraphMetadata"` } func (s *APIV1Service) registerShortcutRoutes(g *echo.Group) { @@ -85,6 +94,11 @@ func (s *APIV1Service) registerShortcutRoutes(g *echo.Group) { Description: create.Description, Visibility: store.Visibility(create.Visibility.String()), Tag: strings.Join(create.Tags, " "), + OpenGraphMetadata: &store.OpenGraphMetadata{ + Title: create.OpenGraphMetadata.Title, + Description: create.OpenGraphMetadata.Description, + Image: create.OpenGraphMetadata.Image, + }, }) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to create shortcut, err: %s", err)).SetInternal(err) @@ -156,6 +170,13 @@ func (s *APIV1Service) registerShortcutRoutes(g *echo.Group) { tag := strings.Join(patch.Tags, " ") shortcutUpdate.Tag = &tag } + if patch.OpenGraphMetadata != nil { + shortcutUpdate.OpenGraphMetadata = &store.OpenGraphMetadata{ + Title: patch.OpenGraphMetadata.Title, + Description: patch.OpenGraphMetadata.Description, + Image: patch.OpenGraphMetadata.Image, + } + } shortcut, err = s.Store.UpdateShortcut(ctx, shortcutUpdate) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to patch shortcut, err: %s", err)).SetInternal(err) @@ -315,6 +336,11 @@ func convertShortcutFromStore(shortcut *store.Shortcut) *Shortcut { Visibility: Visibility(shortcut.Visibility), RowStatus: RowStatus(shortcut.RowStatus), Tags: tags, + OpenGraphMetadata: &OpenGraphMetadata{ + Title: shortcut.OpenGraphMetadata.Title, + Description: shortcut.OpenGraphMetadata.Description, + Image: shortcut.OpenGraphMetadata.Image, + }, } } diff --git a/store/db/migration/dev/LATEST__SCHEMA.sql b/store/db/migration/dev/LATEST__SCHEMA.sql index 0fb43e3..0c2f815 100644 --- a/store/db/migration/dev/LATEST__SCHEMA.sql +++ b/store/db/migration/dev/LATEST__SCHEMA.sql @@ -43,7 +43,8 @@ CREATE TABLE shortcut ( link TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE', - tag TEXT NOT NULL DEFAULT '' + tag TEXT NOT NULL DEFAULT '', + og_metadata TEXT NOT NULL DEFAULT '{}' ); CREATE INDEX idx_shortcut_name ON shortcut(name); diff --git a/store/db/migration/prod/0.3/00__add_og_metadata.sql b/store/db/migration/prod/0.3/00__add_og_metadata.sql new file mode 100644 index 0000000..7de0355 --- /dev/null +++ b/store/db/migration/prod/0.3/00__add_og_metadata.sql @@ -0,0 +1 @@ +ALTER TABLE shortcut ADD COLUMN og_metadata TEXT NOT NULL DEFAULT '{}'; diff --git a/store/db/migration/prod/LATEST__SCHEMA.sql b/store/db/migration/prod/LATEST__SCHEMA.sql index 0fb43e3..0c2f815 100644 --- a/store/db/migration/prod/LATEST__SCHEMA.sql +++ b/store/db/migration/prod/LATEST__SCHEMA.sql @@ -43,7 +43,8 @@ CREATE TABLE shortcut ( link TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE', - tag TEXT NOT NULL DEFAULT '' + tag TEXT NOT NULL DEFAULT '', + og_metadata TEXT NOT NULL DEFAULT '{}' ); CREATE INDEX idx_shortcut_name ON shortcut(name); diff --git a/store/db/seed/10002__shortcut.sql b/store/db/seed/10002__shortcut.sql index 141c266..80dd8ac 100644 --- a/store/db/seed/10002__shortcut.sql +++ b/store/db/seed/10002__shortcut.sql @@ -38,7 +38,8 @@ INSERT INTO `creator_id`, `name`, `link`, - `visibility` + `visibility`, + `og_metadata` ) VALUES ( @@ -46,7 +47,8 @@ VALUES 101, 'schema-change', 'https://www.bytebase.com/blog/how-to-handle-database-schema-change/#what-is-a-database-schema-change', - 'PUBLIC' + 'PUBLIC', + '{"title":"How to Handle Database Migration / Schema Change?","description":"A database schema is the structure of a database, which describes the relationships between the different tables and fields in the database. A database schema change, also known as schema migration, or simply migration refers to any alteration to this structure, such as adding a new table, modifying the data type of a field, or changing the relationships between tables.","image":"https://www.bytebase.com/_next/image/?url=%2Fcontent%2Fblog%2Fhow-to-handle-database-schema-change%2Fchange.webp\u0026w=2048\u0026q=75"}' ); INSERT INTO diff --git a/store/shortcut.go b/store/shortcut.go index 333adfb..e5cf240 100644 --- a/store/shortcut.go +++ b/store/shortcut.go @@ -3,6 +3,7 @@ package store import ( "context" "database/sql" + "encoding/json" "fmt" "strings" ) @@ -31,6 +32,12 @@ func (e Visibility) String() string { return "PRIVATE" } +type OpenGraphMetadata struct { + Title string `json:"title"` + Description string `json:"description"` + Image string `json:"image"` +} + type Shortcut struct { ID int @@ -41,22 +48,24 @@ type Shortcut struct { RowStatus RowStatus // Domain specific fields - Name string - Link string - Description string - Visibility Visibility - Tag string + Name string + Link string + Description string + Visibility Visibility + Tag string + OpenGraphMetadata *OpenGraphMetadata } type UpdateShortcut struct { ID int - RowStatus *RowStatus - Name *string - Link *string - Description *string - Visibility *Visibility - Tag *string + RowStatus *RowStatus + Name *string + Link *string + Description *string + Visibility *Visibility + Tag *string + OpenGraphMetadata *OpenGraphMetadata } type FindShortcut struct { @@ -76,6 +85,15 @@ func (s *Store) CreateShortcut(ctx context.Context, create *Shortcut) (*Shortcut set := []string{"creator_id", "name", "link", "description", "visibility", "tag"} args := []any{create.CreatorID, create.Name, create.Link, create.Description, create.Visibility, create.Tag} placeholder := []string{"?", "?", "?", "?", "?", "?"} + if create.OpenGraphMetadata != nil { + set = append(set, "open_graph_metadata") + openGraphMetadataBytes, err := json.Marshal(create.OpenGraphMetadata) + if err != nil { + return nil, err + } + args = append(args, string(openGraphMetadataBytes)) + placeholder = append(placeholder, "?") + } stmt := ` INSERT INTO shortcut ( @@ -116,6 +134,13 @@ func (s *Store) UpdateShortcut(ctx context.Context, update *UpdateShortcut) (*Sh if update.Tag != nil { set, args = append(set, "tag = ?"), append(args, *update.Tag) } + if update.OpenGraphMetadata != nil { + openGraphMetadataBytes, err := json.Marshal(update.OpenGraphMetadata) + if err != nil { + return nil, err + } + set, args = append(set, "og_metadata = ?"), append(args, string(openGraphMetadataBytes)) + } if len(set) == 0 { return nil, fmt.Errorf("no update specified") } @@ -127,9 +152,10 @@ func (s *Store) UpdateShortcut(ctx context.Context, update *UpdateShortcut) (*Sh ` + strings.Join(set, ", ") + ` WHERE id = ? - RETURNING id, creator_id, created_ts, updated_ts, row_status, name, link, description, visibility, tag + RETURNING id, creator_id, created_ts, updated_ts, row_status, name, link, description, visibility, tag, og_metadata ` shortcut := &Shortcut{} + openGraphMetadataString := "" if err := s.db.QueryRowContext(ctx, stmt, args...).Scan( &shortcut.ID, &shortcut.CreatorID, @@ -141,9 +167,16 @@ func (s *Store) UpdateShortcut(ctx context.Context, update *UpdateShortcut) (*Sh &shortcut.Description, &shortcut.Visibility, &shortcut.Tag, + &openGraphMetadataString, ); err != nil { return nil, err } + if openGraphMetadataString != "" { + shortcut.OpenGraphMetadata = &OpenGraphMetadata{} + if err := json.Unmarshal([]byte(openGraphMetadataString), shortcut.OpenGraphMetadata); err != nil { + return nil, err + } + } s.shortcutCache.Store(shortcut.ID, shortcut) return shortcut, nil @@ -186,7 +219,8 @@ func (s *Store) ListShortcuts(ctx context.Context, find *FindShortcut) ([]*Short link, description, visibility, - tag + tag, + og_metadata FROM shortcut WHERE `+strings.Join(where, " AND ")+` ORDER BY created_ts DESC`, @@ -200,6 +234,7 @@ func (s *Store) ListShortcuts(ctx context.Context, find *FindShortcut) ([]*Short list := make([]*Shortcut, 0) for rows.Next() { shortcut := &Shortcut{} + openGraphMetadataString := "" if err := rows.Scan( &shortcut.ID, &shortcut.CreatorID, @@ -211,9 +246,16 @@ func (s *Store) ListShortcuts(ctx context.Context, find *FindShortcut) ([]*Short &shortcut.Description, &shortcut.Visibility, &shortcut.Tag, + &openGraphMetadataString, ); err != nil { return nil, err } + if openGraphMetadataString != "" { + shortcut.OpenGraphMetadata = &OpenGraphMetadata{} + if err := json.Unmarshal([]byte(openGraphMetadataString), shortcut.OpenGraphMetadata); err != nil { + return nil, err + } + } list = append(list, shortcut) }