diff --git a/api/pkg/db/postgres.go b/api/pkg/db/postgres.go index 672204e..0e9b811 100644 --- a/api/pkg/db/postgres.go +++ b/api/pkg/db/postgres.go @@ -19,6 +19,12 @@ var ( pgOnce sync.Once ) +type OwnerInfo struct { + DisplayName string `json:"display_name"` + Email string `json:"email"` + PhotoLink string `json:"photo_link"` +} + func GetPGClient() (*pgxpool.Config, *pgxpool.Pool) { pgOnce.Do(func() { poolconfig, err := connectPostgres() diff --git a/api/pkg/handlers/account.go b/api/pkg/handlers/account.go index 046b7d0..0c273c7 100644 --- a/api/pkg/handlers/account.go +++ b/api/pkg/handlers/account.go @@ -1,15 +1,19 @@ package handlers import ( + "context" + "database/sql" "errors" "net/http" "github.com/blackmamoth/cloudmesh/pkg/config" + "github.com/blackmamoth/cloudmesh/pkg/db" "github.com/blackmamoth/cloudmesh/pkg/middlewares" "github.com/blackmamoth/cloudmesh/pkg/providers" "github.com/blackmamoth/cloudmesh/pkg/utils" "github.com/blackmamoth/cloudmesh/repository" "github.com/go-chi/chi/v5" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgxpool" "go.uber.org/zap" @@ -31,6 +35,10 @@ type AccountDetail struct { UsedStorage int64 `json:"used_storage"` } +type UnlinkAccountValidation struct { + AccountID string `validate:"required,uuid" json:"account_id"` +} + func NewAccountHandler( connPool *pgxpool.Pool, authMiddleware *middlewares.AuthMiddleware, @@ -48,6 +56,8 @@ func (h *AccountHandler) RegisterRoutes() *chi.Mux { r.Get("/get-accounts", h.getAccounts) + r.Delete("/unlink-account", h.unlinkAccount) + return r } @@ -153,6 +163,97 @@ func (h *AccountHandler) getAccounts(w http.ResponseWriter, r *http.Request) { }) } +func (h *AccountHandler) unlinkAccount(w http.ResponseWriter, r *http.Request) { + payload, ok := utils.ParseAndValidate[UnlinkAccountValidation](w, r, true) + if !ok { + return + } + + accountID, err := db.PGUUID(payload.AccountID) + if err != nil { + config.LOGGER.Error("failed to parse account uuid", zap.Error(err)) + utils.SendAPIErrorResponse( + w, + http.StatusBadRequest, + errors.New("could not validate user credentials"), + ) + } + + userID, ok := r.Context().Value(middlewares.UserKey).(string) + if !ok { + config.LOGGER.Error("invalid userid", zap.Any("user_id_received", userID)) + utils.SendAPIErrorResponse( + w, + http.StatusBadRequest, + errors.New("could not validate user credentials"), + ) + + return + } + + conn, err := h.connPool.Acquire(r.Context()) + if err != nil { + config.LOGGER.Error("failed to acquire new connection from connection pool", zap.Error(err)) + utils.SendAPIErrorResponse( + w, + http.StatusInternalServerError, + errors.New("failed to process your request, please try again later"), + ) + + return + } + defer conn.Release() + + queries := repository.New(conn) + + _, err = queries.GetLinkedAccount(r.Context(), repository.GetLinkedAccountParams{ + UserID: userID, + AccountID: *accountID, + }) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + utils.SendAPIErrorResponse( + w, + http.StatusBadRequest, + "account does not exist or does not belong to the user", + ) + } else { + config.LOGGER.Error("failed to fetch linked account", zap.String("user_id", userID), zap.String("account_id", accountID.String()), zap.Error(err)) + utils.SendAPIErrorResponse(w, http.StatusInternalServerError, "failed to fetch your account details") + } + return + } + + err = utils.WithTransaction(r.Context(), conn, func(ctx context.Context, tx pgx.Tx) error { + qtx := repository.New(conn).WithTx(tx) + + return qtx.DeleteLinkedAccount(ctx, repository.DeleteLinkedAccountParams{ + AccountID: *accountID, + UserID: userID, + }) + }) + if err != nil { + config.LOGGER.Error( + "failed to delete linked account", + zap.String("user_id", userID), + zap.String("account_id", accountID.String()), + zap.Error(err), + ) + utils.SendAPIErrorResponse( + w, + http.StatusInternalServerError, + errors.New("your account could not be deleted"), + ) + return + } + + utils.SendAPIResponse( + w, + http.StatusOK, + map[string]string{"message": "Your account was successfully unlinked"}, + ) +} + func (h *AccountHandler) groupAccountsByProvider( accounts []AccountDetail, ) map[string][]AccountDetail { diff --git a/api/pkg/handlers/files.go b/api/pkg/handlers/files.go index e1a8545..147aaf6 100644 --- a/api/pkg/handlers/files.go +++ b/api/pkg/handlers/files.go @@ -33,14 +33,14 @@ type FilesHandler struct { } type GetFilesValidation struct { - Provider string `validate:"omitempty,oneof=google dropbox" json:"provider"` - ParentFolder string `validate:"omitempty" json:"parent_folder"` - Search string `validate:"omitempty" json:"search"` - SortOn string `validate:"omitempty" json:"sort_on"` - SortBy string `validate:"omitempty" json:"sort_by"` - Limit int32 `validate:"omitempty" json:"limit"` - Offset int32 `validate:"omitempty" json:"offset"` - ContentSearch bool `validate:"omitempty" json:"content_search"` + Provider string `validate:"omitempty,oneof=google dropbox microsoft" json:"provider"` + ParentFolder string `validate:"omitempty" json:"parent_folder"` + Search string `validate:"omitempty" json:"search"` + SortOn string `validate:"omitempty" json:"sort_on"` + SortBy string `validate:"omitempty" json:"sort_by"` + Limit int32 `validate:"omitempty" json:"limit"` + Offset int32 `validate:"omitempty" json:"offset"` + ContentSearch bool `validate:"omitempty" json:"content_search"` } type UploadFilesValidation struct { diff --git a/api/pkg/providers/dropbox.go b/api/pkg/providers/dropbox.go index 8b4ecf5..1767ebf 100644 --- a/api/pkg/providers/dropbox.go +++ b/api/pkg/providers/dropbox.go @@ -57,16 +57,23 @@ type DropboxAccountInfo struct { } type DropboxListFolderEntries struct { - ID string `json:"id"` - Tag string `json:".tag,omitempty"` - Name string `json:"name"` - PathDisplay string `json:"path_display"` - PathLower string `json:"path_lower"` - ClientModified time.Time `json:"client_modified"` - ServerModified time.Time `json:"server_modified"` - ContentHash string `json:"content_hash"` - Revision string `json:"rev"` - Size int `json:"size"` + ID string `json:"id"` + Tag string `json:".tag,omitempty"` + Name string `json:"name"` + PathDisplay string `json:"path_display"` + PathLower string `json:"path_lower"` + ClientModified time.Time `json:"client_modified"` + ServerModified time.Time `json:"server_modified"` + ContentHash string `json:"content_hash"` + Revision string `json:"rev"` + Size int `json:"size"` + SharingInfo *DropboxSharingInfo `json:"sharing_info,omitempty"` +} + +type DropboxSharingInfo struct { + ReadOnly bool `json:"read_only,omitempty"` + ParentSharedFolderID string `json:"parent_shared_folder_id,omitempty"` + ModifiedBy string `json:"modified_by,omitempty"` } type DropboxListFolderResponse struct { @@ -305,6 +312,16 @@ func (p *DropboxProvider) SyncFiles( config.LOGGER.Error("failed to validate access token", zap.Error(err)) } + accountUser, err := queries.GetAccountUserByID(ctx, accountID) + if err != nil { + config.LOGGER.Error( + "failed to fetch account user", + zap.String("account_id", accountID.String()), + zap.Error(err), + ) + return err + } + for { dropboxResponse, err := p.getDropboxFolderList( ctx, @@ -327,6 +344,7 @@ func (p *DropboxProvider) SyncFiles( files, providerFileIDs := p.convertToSyncedItemSlice( dropboxResponse.Entries, accountID, + accountUser, syncDetails.LastSyncedAt.Valid, ) @@ -379,7 +397,9 @@ func (p *DropboxProvider) getDropboxFolderList( accessToken, refreshToken, cursor string, ) (*DropboxListFolderResponse, error) { url := DROPBOX_API_BASE_URL + "/2/files/list_folder" - reqBody := []byte(`{"path": "", "recursive": true, "include_deleted": true}`) + reqBody := []byte( + `{"path": "", "recursive": true, "include_deleted": true, "include_has_explicit_shared_members": true}`, + ) if cursor != "" { url = url + "/continue" @@ -778,7 +798,17 @@ func (p *DropboxProvider) UploadFiles( return err } - files, _ := p.convertToSyncedItemSlice(results, accountID, false) + accountUser, err := queries.GetAccountUserByID(ctx, accountID) + if err != nil { + config.LOGGER.Error( + "failed to fetch account user", + zap.String("account_id", accountID.String()), + zap.Error(err), + ) + return err + } + + files, _ := p.convertToSyncedItemSlice(results, accountID, accountUser, false) _, err = p.bulkInsertSyncedItems(ctx, conn, queries, []string{}, accountID, files, "") if err != nil { @@ -1487,6 +1517,7 @@ func (p *DropboxProvider) bulkInsertSyncedItems( func (p *DropboxProvider) convertToSyncedItemSlice( entries []DropboxListFolderEntries, accountID pgtype.UUID, + accountUser repository.GetAccountUserByIDRow, isValidLastSyncedData bool, ) ([]repository.AddSyncedItemsParams, []string) { syncedItems := []repository.AddSyncedItemsParams{} @@ -1499,8 +1530,22 @@ func (p *DropboxProvider) convertToSyncedItemSlice( parentFolder := path.Dir(entry.PathDisplay) - if entry.Name == "bgnet_usl_c_1.pdf" { - config.LOGGER.Info("is_deleted", zap.String("value", entry.Tag)) + var ownerInfoJSON []byte + if entry.SharingInfo == nil { + ownerInfo := map[string]string{ + "display_name": accountUser.Name, + "email": accountUser.Email, + "photo_link": accountUser.AvatarUrl.String, + } + + var err error + ownerInfoJSON, err = json.Marshal(ownerInfo) + if err != nil { + config.LOGGER.Error("failed to marshal owner info", zap.Error(err)) + ownerInfoJSON = []byte("{}") + } + } else { + ownerInfoJSON = []byte("{}") } syncedItems = append(syncedItems, repository.AddSyncedItemsParams{ @@ -1515,6 +1560,7 @@ func (p *DropboxProvider) convertToSyncedItemSlice( IsFolder: entry.Tag == "folder", ContentHash: db.PGTextField(entry.ContentHash), IsTrashed: db.PGBool(entry.Tag == "deleted"), + OwnerInfo: ownerInfoJSON, CreatedTime: db.PGTimestamptzField(time.Time{}), ModifiedTime: db.PGTimestamptzField(entry.ClientModified), ThumbnailLink: db.PGTextField(""), diff --git a/api/pkg/providers/google.go b/api/pkg/providers/google.go index 1e2f459..4579ca9 100644 --- a/api/pkg/providers/google.go +++ b/api/pkg/providers/google.go @@ -219,7 +219,7 @@ func (p *GoogleProvider) SyncFiles( fileList, err := driveService.Files. List(). Q(query). - Fields("files(id, name, size, mimeType, createdTime, modifiedTime, thumbnailLink, fullFileExtension, parents, webViewLink, webContentLink, iconLink, sha256Checksum, trashed)"). + Fields("files(id, name, size, mimeType, createdTime, modifiedTime, thumbnailLink, fullFileExtension, parents, webViewLink, webContentLink, iconLink, sha256Checksum, trashed,owners,ownedByMe)"). PageToken(pageToken). PageSize(1000). Do() @@ -1070,6 +1070,24 @@ func (p *GoogleProvider) convertToSyncedItemSlice( parentFolder = file.Parents[0] } + var ownerInfoJSON []byte + if len(file.Owners) > 0 { + ownerInfo := map[string]string{ + "display_name": file.Owners[0].DisplayName, + "email": file.Owners[0].EmailAddress, + "photo_link": file.Owners[0].PhotoLink, + } + + var err error + ownerInfoJSON, err = json.Marshal(ownerInfo) + if err != nil { + config.LOGGER.Error("failed to marshal owner info", zap.Error(err)) + ownerInfoJSON = []byte("{}") + } + } else { + ownerInfoJSON = []byte("{}") + } + syncedItems = append(syncedItems, repository.AddSyncedItemsParams{ AccountID: accountID, ProviderFileID: file.Id, @@ -1082,6 +1100,7 @@ func (p *GoogleProvider) convertToSyncedItemSlice( ParentFolder: db.PGTextField(parentFolder), IsFolder: isFolder, IsTrashed: db.PGBool(file.Trashed), + OwnerInfo: ownerInfoJSON, ContentHash: db.PGTextField(file.Sha256Checksum), ThumbnailLink: db.PGTextField(file.ThumbnailLink), PreviewLink: db.PGTextField(previewLink), @@ -1208,7 +1227,7 @@ func (p *GoogleProvider) CreateFolder( } folder, err := driveService.Files.Create(&driveFolder). - Fields("id, name, size, mimeType, createdTime, modifiedTime, thumbnailLink, fullFileExtension, parents, webViewLink, webContentLink, iconLink, sha256Checksum, trashed"). + Fields("id, name, size, mimeType, createdTime, modifiedTime, thumbnailLink, fullFileExtension, parents, webViewLink, webContentLink, iconLink, sha256Checksum, trashed, owners, ownedByMe"). Do() if err != nil { logFields = append(logFields, zap.Error(err)) diff --git a/api/pkg/providers/microsoft.go b/api/pkg/providers/microsoft.go index 1e3b26c..1288cf7 100644 --- a/api/pkg/providers/microsoft.go +++ b/api/pkg/providers/microsoft.go @@ -525,7 +525,14 @@ func (p *MicrosoftProvider) convertToSyncedItemSlice( syncedItems := []repository.AddSyncedItemsParams{} providerFileIDs := []string{} + rootFolderID := "" + for _, item := range items { + if item.Name == "root" && item.ParentReference.Path == "" { + rootFolderID = item.ID + continue + } + ext := filepath.Ext(item.Name) mimeType := mime.TypeByExtension(ext) @@ -539,7 +546,7 @@ func (p *MicrosoftProvider) convertToSyncedItemSlice( parentFolder := "/" - if item.ParentReference.ID != "" { + if item.ParentReference.ID != rootFolderID { parentFolder = item.ParentReference.ID } @@ -548,6 +555,24 @@ func (p *MicrosoftProvider) convertToSyncedItemSlice( path = "/" } + var ownerInfoJSON []byte + if item.CreatedBy.User.ID != "" { + ownerInfo := map[string]string{ + "display_name": item.CreatedBy.User.DisplayName, + "email": item.CreatedBy.User.Email, + "photo_link": "", + } + + var err error + ownerInfoJSON, err = json.Marshal(ownerInfo) + if err != nil { + config.LOGGER.Error("failed to marshal owner info", zap.Error(err)) + ownerInfoJSON = []byte("{}") + } + } else { + ownerInfoJSON = []byte("{}") + } + syncedItems = append(syncedItems, repository.AddSyncedItemsParams{ AccountID: accountID, ProviderFileID: item.ID, @@ -558,6 +583,7 @@ func (p *MicrosoftProvider) convertToSyncedItemSlice( MimeType: db.PGTextField(mimeType), ParentFolder: db.PGTextField(parentFolder), IsFolder: item.Folder != nil, + OwnerInfo: ownerInfoJSON, ContentHash: db.PGTextField(contentHash), CreatedTime: db.PGTimestamptzField(item.CreatedDateTime), ModifiedTime: db.PGTimestamptzField(item.LastModifiedDateTime), diff --git a/api/repository/copyfrom.go b/api/repository/copyfrom.go index 9bed49c..1fac38e 100644 --- a/api/repository/copyfrom.go +++ b/api/repository/copyfrom.go @@ -39,6 +39,7 @@ func (r iteratorForAddSyncedItems) Values() ([]interface{}, error) { r.rows[0].ParentFolder, r.rows[0].IsFolder, r.rows[0].IsTrashed, + r.rows[0].OwnerInfo, r.rows[0].ContentHash, r.rows[0].CreatedTime, r.rows[0].ModifiedTime, @@ -55,5 +56,5 @@ func (r iteratorForAddSyncedItems) Err() error { } func (q *Queries) AddSyncedItems(ctx context.Context, arg []AddSyncedItemsParams) (int64, error) { - return q.db.CopyFrom(ctx, []string{"synced_items"}, []string{"account_id", "provider_file_id", "name", "extension", "size", "path", "mime_type", "parent_folder", "is_folder", "is_trashed", "content_hash", "created_time", "modified_time", "thumbnail_link", "preview_link", "web_view_link", "web_content_link", "link_expires_at"}, &iteratorForAddSyncedItems{rows: arg}) + return q.db.CopyFrom(ctx, []string{"synced_items"}, []string{"account_id", "provider_file_id", "name", "extension", "size", "path", "mime_type", "parent_folder", "is_folder", "is_trashed", "owner_info", "content_hash", "created_time", "modified_time", "thumbnail_link", "preview_link", "web_view_link", "web_content_link", "link_expires_at"}, &iteratorForAddSyncedItems{rows: arg}) } diff --git a/api/repository/linked_account.sql.go b/api/repository/linked_account.sql.go index 08e9602..1bb7c31 100644 --- a/api/repository/linked_account.sql.go +++ b/api/repository/linked_account.sql.go @@ -50,6 +50,20 @@ func (q *Queries) AddAccountDetails(ctx context.Context, arg AddAccountDetailsPa return id, err } +const deleteLinkedAccount = `-- name: DeleteLinkedAccount :exec +DELETE FROM linked_account WHERE id = $1 AND user_id = $2 +` + +type DeleteLinkedAccountParams struct { + AccountID pgtype.UUID `json:"account_id"` + UserID string `json:"user_id"` +} + +func (q *Queries) DeleteLinkedAccount(ctx context.Context, arg DeleteLinkedAccountParams) error { + _, err := q.db.Exec(ctx, deleteLinkedAccount, arg.AccountID, arg.UserID) + return err +} + const getAccountByProviderID = `-- name: GetAccountByProviderID :one SELECT id FROM linked_account WHERE user_id = $1 AND provider = $2 AND provider_user_id = $3 LIMIT 1 ` @@ -67,6 +81,23 @@ func (q *Queries) GetAccountByProviderID(ctx context.Context, arg GetAccountByPr return id, err } +const getAccountUserByID = `-- name: GetAccountUserByID :one +SELECT name, email, avatar_url FROM linked_account WHERE id = $1 +` + +type GetAccountUserByIDRow struct { + Name string `json:"name"` + Email string `json:"email"` + AvatarUrl pgtype.Text `json:"avatar_url"` +} + +func (q *Queries) GetAccountUserByID(ctx context.Context, accountID pgtype.UUID) (GetAccountUserByIDRow, error) { + row := q.db.QueryRow(ctx, getAccountUserByID, accountID) + var i GetAccountUserByIDRow + err := row.Scan(&i.Name, &i.Email, &i.AvatarUrl) + return i, err +} + const getAuthTokenExpiry = `-- name: GetAuthTokenExpiry :one SELECT expiry FROM linked_account WHERE id = $1 ` diff --git a/api/repository/models.go b/api/repository/models.go index 4d3ef5f..3df75ab 100644 --- a/api/repository/models.go +++ b/api/repository/models.go @@ -6,6 +6,7 @@ package repository import ( "database/sql/driver" + "encoding/json" "fmt" "github.com/jackc/pgx/v5/pgtype" @@ -168,6 +169,7 @@ type SyncedItem struct { ParentFolder pgtype.Text `json:"parent_folder"` IsFolder bool `json:"is_folder"` IsTrashed pgtype.Bool `json:"is_trashed"` + OwnerInfo json.RawMessage `json:"owner_info"` MimeType pgtype.Text `json:"mime_type"` CreatedTime pgtype.Timestamptz `json:"created_time"` ModifiedTime pgtype.Timestamptz `json:"modified_time"` diff --git a/api/repository/synced_items.sql.go b/api/repository/synced_items.sql.go index 811e67f..7fa9d6b 100644 --- a/api/repository/synced_items.sql.go +++ b/api/repository/synced_items.sql.go @@ -7,6 +7,7 @@ package repository import ( "context" + "encoding/json" "github.com/jackc/pgx/v5/pgtype" ) @@ -22,6 +23,7 @@ type AddSyncedItemsParams struct { ParentFolder pgtype.Text `json:"parent_folder"` IsFolder bool `json:"is_folder"` IsTrashed pgtype.Bool `json:"is_trashed"` + OwnerInfo json.RawMessage `json:"owner_info"` ContentHash pgtype.Text `json:"content_hash"` CreatedTime pgtype.Timestamptz `json:"created_time"` ModifiedTime pgtype.Timestamptz `json:"modified_time"` @@ -184,9 +186,7 @@ SELECT synced_items.id, synced_items.web_view_link, synced_items.web_content_link, synced_items.modified_time, - linked_account.name AS account_name, - linked_account.avatar_url, - linked_account.provider + synced_items.owner_info FROM synced_items JOIN linked_account ON linked_account.id = synced_items.account_id @@ -252,9 +252,7 @@ type GetSyncedItemsRow struct { WebViewLink pgtype.Text `json:"web_view_link"` WebContentLink pgtype.Text `json:"web_content_link"` ModifiedTime pgtype.Timestamptz `json:"modified_time"` - AccountName string `json:"account_name"` - AvatarUrl pgtype.Text `json:"avatar_url"` - Provider ProviderEnum `json:"provider"` + OwnerInfo json.RawMessage `json:"owner_info"` } func (q *Queries) GetSyncedItems(ctx context.Context, arg GetSyncedItemsParams) ([]GetSyncedItemsRow, error) { @@ -287,9 +285,7 @@ func (q *Queries) GetSyncedItems(ctx context.Context, arg GetSyncedItemsParams) &i.WebViewLink, &i.WebContentLink, &i.ModifiedTime, - &i.AccountName, - &i.AvatarUrl, - &i.Provider, + &i.OwnerInfo, ); err != nil { return nil, err } diff --git a/api/sqlc.yml b/api/sqlc.yml index 2fc7662..4517800 100644 --- a/api/sqlc.yml +++ b/api/sqlc.yml @@ -13,3 +13,8 @@ sql: emit_json_tags: true emit_empty_slices: true sql_package: "pgx/v5" + overrides: + - column: "synced_items.owner_info" + go_type: + import: "encoding/json" + type: "RawMessage" diff --git a/api/sqlc/migrations/20250707160730_add_synced_items_table.sql b/api/sqlc/migrations/20250707160730_add_synced_items_table.sql index bbaba42..cf9b67b 100644 --- a/api/sqlc/migrations/20250707160730_add_synced_items_table.sql +++ b/api/sqlc/migrations/20250707160730_add_synced_items_table.sql @@ -14,6 +14,8 @@ CREATE TABLE IF NOT EXISTS synced_items ( is_folder BOOLEAN NOT NULL, is_trashed BOOLEAN DEFAULT FALSE, + owner_info JSONB DEFAULT '{"display_name": null, "email": null, "photo_link": null}'::jsonb, + mime_type TEXT DEFAULT NULL, created_time TIMESTAMPTZ DEFAULT NULL, modified_time TIMESTAMPTZ DEFAULT NULL, diff --git a/api/sqlc/queries/linked_account.sql b/api/sqlc/queries/linked_account.sql index 3034881..5a60280 100644 --- a/api/sqlc/queries/linked_account.sql +++ b/api/sqlc/queries/linked_account.sql @@ -38,3 +38,9 @@ SELECT last_synced_at FROM linked_account WHERE user_id = @user_id ORDER BY last -- name: GetLinkedAccount :one SELECT id, provider, access_token, refresh_token FROM linked_account WHERE user_id = @user_id AND id = @account_id; + +-- name: DeleteLinkedAccount :exec +DELETE FROM linked_account WHERE id = @account_id AND user_id = @user_id; + +-- name: GetAccountUserByID :one +SELECT name, email, avatar_url FROM linked_account WHERE id = @account_id; diff --git a/api/sqlc/queries/synced_items.sql b/api/sqlc/queries/synced_items.sql index d53d04c..00a5799 100644 --- a/api/sqlc/queries/synced_items.sql +++ b/api/sqlc/queries/synced_items.sql @@ -1,8 +1,8 @@ -- name: AddSyncedItems :copyfrom INSERT INTO synced_items ( - account_id, provider_file_id, name, extension, size, path, mime_type, parent_folder, is_folder, is_trashed, content_hash, created_time, modified_time, thumbnail_link, preview_link, web_view_link, web_content_link, link_expires_at + account_id, provider_file_id, name, extension, size, path, mime_type, parent_folder, is_folder, is_trashed, owner_info, content_hash, created_time, modified_time, thumbnail_link, preview_link, web_view_link, web_content_link, link_expires_at ) VALUES ( - @account_id, @provider_file_id, @name, @extension , @size, @path, @mime_type, @parent_folder, @is_folder, @is_trashed, @content_hash, @created_time, @modified_time, @thumbnail_link, @preview_link, @web_view_link, @web_content_link, @link_expires_at + @account_id, @provider_file_id, @name, @extension , @size, @path, @mime_type, @parent_folder, @is_folder, @is_trashed, @owner_info, @content_hash, @created_time, @modified_time, @thumbnail_link, @preview_link, @web_view_link, @web_content_link, @link_expires_at ); -- name: GetSyncedItems :many @@ -16,9 +16,7 @@ SELECT synced_items.id, synced_items.web_view_link, synced_items.web_content_link, synced_items.modified_time, - linked_account.name AS account_name, - linked_account.avatar_url, - linked_account.provider + synced_items.owner_info FROM synced_items JOIN linked_account ON linked_account.id = synced_items.account_id