Skip to content

Commit

Permalink
define a more robust asset schema and expose media assets on thread/p…
Browse files Browse the repository at this point in the history
…ost APIs
  • Loading branch information
Southclaws committed Jul 14, 2023
1 parent 73200ff commit 1f50f52
Show file tree
Hide file tree
Showing 41 changed files with 4,758 additions and 167 deletions.
42 changes: 41 additions & 1 deletion api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,7 @@ paths:
equivalent of S3's pre-signed upload URL. Files uploaded to this
endpoint will be stored on the local filesystem instead of the cloud.
tags: [assets]
parameters: [$ref: "#/components/parameters/PostIDQueryParam"]
requestBody: { $ref: "#/components/requestBodies/AssetUpload" }
responses:
default: { $ref: "#/components/responses/InternalServerError" }
Expand Down Expand Up @@ -647,6 +648,14 @@ components:
schema:
$ref: "#/components/schemas/Identifier"

PostIDQueryParam:
description: Unique post ID.
name: post_id
in: query
required: true
schema:
$ref: "#/components/schemas/Identifier"

OAuthProvider:
description: The identifier for an OAuth2 provider such as "twitter".
name: oauth_provider
Expand Down Expand Up @@ -1635,6 +1644,7 @@ components:
- category
- reacts
- meta
- media
properties:
title:
type: string
Expand Down Expand Up @@ -1663,6 +1673,7 @@ components:
reacts:
$ref: "#/components/schemas/ReactList"
meta: { $ref: "#/components/schemas/Metadata" }
media: { $ref: "#/components/schemas/MediaItemList" }

ThreadList:
type: array
Expand Down Expand Up @@ -1708,7 +1719,7 @@ components:

PostCommonProps:
type: object
required: [root_id, root_slug, body, author, reacts]
required: [root_id, root_slug, body, author, reacts, media]
properties:
root_id: { $ref: "#/components/schemas/Identifier" }
root_slug: { $ref: "#/components/schemas/ThreadMark" }
Expand All @@ -1717,6 +1728,7 @@ components:
meta: { $ref: "#/components/schemas/Metadata" }
reacts: { $ref: "#/components/schemas/ReactList" }
reply_to: { $ref: "#/components/schemas/Identifier" }
media: { $ref: "#/components/schemas/MediaItemList" }

PostInitialProps:
type: object
Expand Down Expand Up @@ -1770,13 +1782,41 @@ components:
url:
type: string

#
# 888b d888 888 d8b
# 8888b d8888 888 Y8P
# 88888b.d88888 888
# 888Y88888P888 .d88b. .d88888 888 8888b.
# 888 Y888P 888 d8P Y8b d88" 888 888 "88b
# 888 Y8P 888 88888888 888 888 888 .d888888
# 888 " 888 Y8b. Y88b 888 888 888 888
# 888 888 "Y8888 "Y88888 888 "Y888888
#

Asset:
type: object
required: [url]
properties:
url:
type: string

MediaItemList:
type: array
items: { $ref: "#/components/schemas/MediaItem" }

MediaItem:
type: object
required: [url, mime_type, width, height]
properties:
url:
type: string
mime_type:
type: string
width:
type: number
height:
type: number

securitySchemes:
browser:
type: apiKey
Expand Down
21 changes: 21 additions & 0 deletions app/resources/asset/dto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package asset

import (
"github.com/Southclaws/storyden/internal/ent"
)

type Asset struct {
URL string
MIMEType string
Width int
Height int
}

func FromModel(a *ent.Asset) *Asset {
return &Asset{
URL: a.URL,
MIMEType: a.Mimetype,
Width: a.Width,
Height: a.Height,
}
}
3 changes: 3 additions & 0 deletions app/resources/post/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ func (d *database) Create(
WithRoot(func(pq *ent.PostQuery) {
pq.WithAuthor()
}).
WithAssets().
Only(ctx)
if err != nil {
if ent.IsNotFound(err) {
Expand All @@ -99,6 +100,7 @@ func (d *database) Get(ctx context.Context, id PostID) (*Post, error) {
WithRoot(func(pq *ent.PostQuery) {
pq.WithAuthor()
}).
WithAssets().
Only(ctx)
if err != nil {
return nil, fault.Wrap(err, fctx.With(ctx), ftag.With(ftag.Internal))
Expand Down Expand Up @@ -127,6 +129,7 @@ func (d *database) Update(ctx context.Context, id PostID, opts ...Option) (*Post
WithRoot(func(pq *ent.PostQuery) {
pq.WithAuthor()
}).
WithAssets().
Only(ctx)
if err != nil {
return nil, fault.Wrap(err, fctx.With(ctx), ftag.With(ftag.Internal))
Expand Down
3 changes: 3 additions & 0 deletions app/resources/post/dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/rs/xid"

"github.com/Southclaws/storyden/app/resources/account"
"github.com/Southclaws/storyden/app/resources/asset"
"github.com/Southclaws/storyden/app/resources/react"
"github.com/Southclaws/storyden/internal/ent"
)
Expand All @@ -28,6 +29,7 @@ type Post struct {
ReplyTo opt.Optional[PostID]
Reacts []*react.React
Meta map[string]any
Assets []*asset.Asset

CreatedAt time.Time
UpdatedAt time.Time
Expand Down Expand Up @@ -74,6 +76,7 @@ func FromModel(m *ent.Post) (w *Post) {
ReplyTo: replyTo,
Reacts: dt.Map(m.Edges.Reacts, react.FromModel),
Meta: m.Metadata,
Assets: dt.Map(m.Edges.Assets, asset.FromModel),

CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
Expand Down
6 changes: 6 additions & 0 deletions app/resources/post/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,9 @@ func WithMeta(meta map[string]any) Option {
m.SetMetadata(meta)
}
}

func WithAssets(ids ...xid.ID) Option {
return func(m *ent.PostMutation) {
m.AddAssetIDs(ids...)
}
}
3 changes: 3 additions & 0 deletions app/resources/thread/dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/Southclaws/opt"

"github.com/Southclaws/storyden/app/resources/account"
"github.com/Southclaws/storyden/app/resources/asset"
"github.com/Southclaws/storyden/app/resources/category"
"github.com/Southclaws/storyden/app/resources/post"
"github.com/Southclaws/storyden/app/resources/react"
Expand All @@ -31,6 +32,7 @@ type Thread struct {
Posts []*post.Post
Reacts []*react.React
Meta map[string]any
Assets []*asset.Asset
}

func (*Thread) GetResourceName() string { return "thread" }
Expand Down Expand Up @@ -75,5 +77,6 @@ func FromModel(m *ent.Post) *Thread {
Posts: posts,
Reacts: dt.Map(m.Edges.Reacts, react.FromModel),
Meta: m.Metadata,
Assets: dt.Map(m.Edges.Assets, asset.FromModel),
}
}
103 changes: 103 additions & 0 deletions app/services/asset/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package asset

import (
"context"
"fmt"
"io"
"path/filepath"

"github.com/Southclaws/fault"
"github.com/Southclaws/fault/fctx"
"github.com/rs/xid"
"go.uber.org/fx"
"go.uber.org/zap"

"github.com/Southclaws/storyden/app/resources/account"
"github.com/Southclaws/storyden/app/resources/post"
"github.com/Southclaws/storyden/app/resources/rbac"
"github.com/Southclaws/storyden/app/resources/thread"
"github.com/Southclaws/storyden/app/services/authentication"
"github.com/Southclaws/storyden/internal/config"
"github.com/Southclaws/storyden/internal/object"
)

const assetsSubdirectory = "assets"

type Service interface {
Upload(ctx context.Context, pid post.PostID, r io.Reader) (string, error)
Read(ctx context.Context, path string) (io.Reader, error)
}

func Build() fx.Option {
return fx.Provide(New)
}

type service struct {
l *zap.Logger
rbac rbac.AccessManager

account_repo account.Repository
thread_repo thread.Repository
post_repo post.Repository

os object.Storer

address string
}

func New(
l *zap.Logger,
rbac rbac.AccessManager,

account_repo account.Repository,
thread_repo thread.Repository,
post_repo post.Repository,

os object.Storer,
cfg config.Config,
) Service {
return &service{
l: l.With(zap.String("service", "post")),
rbac: rbac,
account_repo: account_repo,
thread_repo: thread_repo,
post_repo: post_repo,
os: os,
address: cfg.PublicWebAddress,
}
}

func (s *service) Upload(ctx context.Context, pid post.PostID, r io.Reader) (string, error) {
accountID, err := authentication.GetAccountID(ctx)
if err != nil {
return "", fault.Wrap(err, fctx.With(ctx))
}

assetID := fmt.Sprintf("%s-%s", accountID.String(), xid.New().String())
path := filepath.Join(assetsSubdirectory, assetID)

if err := s.os.Write(ctx, path, r); err != nil {
return "", fault.Wrap(err, fctx.With(ctx))
}

url := fmt.Sprintf("%s/api/v1/assets/%s", s.address, assetID)

_, err = s.post_repo.Update(ctx, pid, post.WithAssets(
// TODO: Insert asset record and append to post.
))
if err != nil {
return "", fault.Wrap(err, fctx.With(ctx))
}

return url, nil
}

func (s *service) Read(ctx context.Context, assetID string) (io.Reader, error) {
path := filepath.Join(assetsSubdirectory, assetID)
r, err := s.os.Read(ctx, path)
if err != nil {
return nil, fault.Wrap(err, fctx.With(ctx))
}

return r, nil
}
35 changes: 10 additions & 25 deletions app/transports/openapi/bindings/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,27 @@ package bindings
import (
"context"
"fmt"
"path/filepath"

"github.com/Southclaws/fault"
"github.com/Southclaws/fault/fctx"
"github.com/rs/xid"

"github.com/Southclaws/storyden/app/services/authentication"
"github.com/Southclaws/storyden/internal/config"
"github.com/Southclaws/storyden/internal/object"
"github.com/Southclaws/storyden/app/resources/post"
"github.com/Southclaws/storyden/app/services/asset"
"github.com/Southclaws/storyden/internal/openapi"
)

type Assets struct {
os object.Storer
address string
a asset.Service
}

func NewAssets(cfg config.Config, os object.Storer) Assets {
return Assets{os, cfg.PublicWebAddress}
func NewAssets(a asset.Service) Assets {
return Assets{a}
}

const assetsSubdirectory = "assets"

func (i *Assets) AssetGetUploadURL(ctx context.Context, request openapi.AssetGetUploadURLRequestObject) (openapi.AssetGetUploadURLResponseObject, error) {
// TODO: Check if S3 is available and create a pre-signed upload URL if so.

url := fmt.Sprintf("%s/api/v1/assets", i.address)
url := fmt.Sprintf("%s/api/v1/assets", "i.address")

return openapi.AssetGetUploadURL200JSONResponse{
AssetGetUploadURLOKJSONResponse: openapi.AssetGetUploadURLOKJSONResponse{
Expand All @@ -39,9 +33,7 @@ func (i *Assets) AssetGetUploadURL(ctx context.Context, request openapi.AssetGet
}

func (i *Assets) AssetGet(ctx context.Context, request openapi.AssetGetRequestObject) (openapi.AssetGetResponseObject, error) {
path := filepath.Join(assetsSubdirectory, request.Id)

r, err := i.os.Read(ctx, path)
r, err := i.a.Read(ctx, request.Id)
if err != nil {
return nil, fault.Wrap(err, fctx.With(ctx))
}
Expand All @@ -54,20 +46,13 @@ func (i *Assets) AssetGet(ctx context.Context, request openapi.AssetGetRequestOb
}

func (i *Assets) AssetUpload(ctx context.Context, request openapi.AssetUploadRequestObject) (openapi.AssetUploadResponseObject, error) {
accountID, err := authentication.GetAccountID(ctx)
if err != nil {
return nil, fault.Wrap(err, fctx.With(ctx))
}

assetID := fmt.Sprintf("%s-%s", accountID.String(), xid.New().String())
path := filepath.Join(assetsSubdirectory, assetID)
postID := openapi.ParseID(request.Params.PostId)

if err := i.os.Write(ctx, path, request.Body); err != nil {
url, err := i.a.Upload(ctx, post.PostID(postID), request.Body)
if err != nil {
return nil, fault.Wrap(err, fctx.With(ctx))
}

url := fmt.Sprintf("%s/api/v1/assets/%s", i.address, assetID)

return openapi.AssetUpload200JSONResponse{
AssetUploadOKJSONResponse: openapi.AssetUploadOKJSONResponse{
Url: url,
Expand Down
Loading

0 comments on commit 1f50f52

Please sign in to comment.