diff --git a/ext/sheets/larkdrive/client.go b/ext/sheets/larkdrive/client.go new file mode 100644 index 0000000000..e599893cc5 --- /dev/null +++ b/ext/sheets/larkdrive/client.go @@ -0,0 +1,152 @@ +package larkdrive + +import ( + "context" + "fmt" + "regexp" + + "github.com/goto/optimus/ext/sheets/lark" + "github.com/goto/optimus/internal/errors" + "github.com/goto/salt/log" + + larksdk "github.com/larksuite/oapi-sdk-go/v3" + larkdrive "github.com/larksuite/oapi-sdk-go/v3/service/drive/v1" +) + +const EntityLarkDrive = "larkdrive" + +var driveIDRegexp = regexp.MustCompile(`larksuite\.com\/drive\/folder\/([a-zA-Z0-9]+)`) + +type SheetCallback func(file *Sheet) error + +type Client struct { + client *larksdk.Client + sheetClient *lark.Client +} + +func (*Client) GetFolderToken(url string) (string, error) { + var folderToken string + + if !driveIDRegexp.MatchString(url) { + return folderToken, errors.NotFound(EntityLarkDrive, "invalid Lark Drive URL format") + } + + subMatched := driveIDRegexp.FindStringSubmatch(url) + if len(subMatched) > 1 { + folderToken = subMatched[1] + return folderToken, nil + } + + return folderToken, errors.NotFound(EntityLarkDrive, "failed to extract folder token from URL") +} + +func (c *Client) IterateSheet(ctx context.Context, folderToken string, callback SheetCallback) error { + builder := larkdrive.NewListFileReqBuilder(). + PageSize(100). + FolderToken(folderToken). + OrderBy("EditedTime"). + Direction("DESC") + + for { + req := builder.Build() + + resp, err := c.client.Drive.File.List(ctx, req) + if err != nil { + return errors.AddErrContext(err, EntityLarkDrive, "failed to list files from lark folder") + } + + if !resp.Success() { + return errors.NewError(ErrCodeToErrorType(resp.CodeError), EntityLarkDrive, fmt.Sprintf("failed to list files with error: %s", resp.ErrorResp())) + } + + for _, file := range resp.Data.Files { + sheet, err := NewSheet(c.sheetClient, file) + if err != nil { + continue + } + + if err := callback(sheet); err != nil { + return err + } + } + + hasMoreData := resp.Data.HasMore != nil && *resp.Data.HasMore + hasNextPageToken := resp.Data.NextPageToken == nil + hasNextPage := hasMoreData && hasNextPageToken + if hasNextPage { + break + } + + builder.PageToken(*resp.Data.NextPageToken) + } + + return nil +} + +func (c *Client) GetRevisionID(ctx context.Context, url string) (int, error) { + folderToken, err := c.GetFolderToken(url) + if err != nil { + return 0, err + } + + builder := larkdrive.NewListFileReqBuilder(). + PageSize(100). + FolderToken(folderToken). + OrderBy("EditedTime"). + Direction("DESC") + revisionNumbers := make([]int, 0) + + for { + req := builder.Build() + + resp, err := c.client.Drive.File.List(ctx, req) + if err != nil { + return 0, errors.AddErrContext(err, EntityLarkDrive, "failed to list files from lark folder") + } + + if !resp.Success() { + return 0, errors.NewError(ErrCodeToErrorType(resp.CodeError), EntityLarkDrive, fmt.Sprintf("failed to list files with error: %s", resp.ErrorResp())) + } + + for _, file := range resp.Data.Files { + sheet, err := NewSheet(c.sheetClient, file) + if err != nil { + continue + } + + revisionId, err := sheet.GetRevisionID(ctx, *sheet.Url) + if err != nil { + return 0, errors.AddErrContext(err, EntityLarkDrive, fmt.Sprintf("failed to get revision ID for file: %s", *file.Name)) + } + revisionNumbers = append(revisionNumbers, revisionId) + } + + hasMoreData := resp.Data.HasMore != nil && *resp.Data.HasMore + hasNextPageToken := resp.Data.NextPageToken == nil + hasNextPage := hasMoreData && hasNextPageToken + if hasNextPage { + break + } + + builder.PageToken(*resp.Data.NextPageToken) + } + + revisionNumber := GenerateRevNumForDrive(revisionNumbers) + return revisionNumber, nil +} + +func NewClient(secret string, logger log.Logger) (*Client, error) { + cred, err := NewCredentialFromSecret(secret) + if err != nil { + return nil, err + } + + options := []larksdk.ClientOptionFunc{ + larksdk.WithLogger(NewLogger(logger)), + } + client := larksdk.NewClient(cred.AppID, cred.AppSecret, options...) + + return &Client{ + client: client, + }, nil +} diff --git a/ext/sheets/larkdrive/client_test.go b/ext/sheets/larkdrive/client_test.go new file mode 100644 index 0000000000..9528c46ed3 --- /dev/null +++ b/ext/sheets/larkdrive/client_test.go @@ -0,0 +1,70 @@ +package larkdrive + +import ( + "testing" + + "github.com/goto/optimus/ext/sheets/lark" + + larksdk "github.com/larksuite/oapi-sdk-go/v3" +) + +func TestClient_GetFolderToken(t *testing.T) { + type fields struct { + client *larksdk.Client + sheetClient *lark.Client + } + type args struct { + url string + } + tests := []struct { + name string + fields fields + args args + want string + wantErr bool + }{ + { + name: "Extract folder token from valid URL without subsdomain", + fields: fields{}, + args: args{ + url: "https://larksuite.com/drive/folder/1234567890abcdef", + }, + want: "1234567890abcdef", + wantErr: false, + }, + { + name: "Extract folder token from valid URL with subsdomain", + fields: fields{}, + args: args{ + url: "https://subdomain.larksuite.com/drive/folder/1234567890abcdef", + }, + want: "1234567890abcdef", + wantErr: false, + }, + { + name: "Invalid URL format", + fields: fields{}, + args: args{ + url: "https://drive.google.com/drive/folder/1234567890abcdef", + }, + want: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cl := &Client{ + client: tt.fields.client, + sheetClient: tt.fields.sheetClient, + } + got, err := cl.GetFolderToken(tt.args.url) + if (err != nil) != tt.wantErr { + t.Errorf("GetFolderToken() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("GetFolderToken() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/ext/sheets/larkdrive/dto.go b/ext/sheets/larkdrive/dto.go new file mode 100644 index 0000000000..fab858ed20 --- /dev/null +++ b/ext/sheets/larkdrive/dto.go @@ -0,0 +1,41 @@ +package larkdrive + +import ( + "encoding/json" + + "github.com/goto/optimus/internal/errors" + + larkcore "github.com/larksuite/oapi-sdk-go/v3/core" +) + +const ( + ErrCodeInvalidParam = 1061002 + ErrCodeNotFound = 1061003 + ErrCodeForbidden = 1061004 +) + +func ErrCodeToErrorType(codeError larkcore.CodeError) errors.ErrorType { + switch codeError.Code { + case ErrCodeInvalidParam: + return errors.ErrInvalidArgument + case ErrCodeNotFound: + return errors.ErrNotFound + case ErrCodeForbidden: + return errors.ErrForbidden + default: + return errors.ErrInternalError + } +} + +type Credential struct { + AppID string `json:"app_id"` + AppSecret string `json:"app_secret"` +} + +func NewCredentialFromSecret(secret string) (*Credential, error) { + cred := &Credential{} + if err := json.Unmarshal([]byte(secret), cred); err != nil { + return nil, errors.AddErrContextWithType(err, errors.ErrInvalidArgument, EntityLarkDrive, "invalid secret format, expected JSON") + } + return cred, nil +} diff --git a/ext/sheets/larkdrive/file.go b/ext/sheets/larkdrive/file.go new file mode 100644 index 0000000000..bd4a9d2e3a --- /dev/null +++ b/ext/sheets/larkdrive/file.go @@ -0,0 +1,76 @@ +package larkdrive + +import ( + "bytes" + "encoding/binary" + "hash/crc64" + "sort" + + larksheet "github.com/goto/optimus/ext/sheets/lark" + "github.com/goto/optimus/internal/errors" + + larkdrive "github.com/larksuite/oapi-sdk-go/v3/service/drive/v1" +) + +// LarkFileType enum can be referred to https://open.larksuite.com/document/server-docs/docs/drive-v1/folder/list on data.files[].type + +const ( + LarkFileTypeSheet = "sheet" +) + +// Sheet represents from Lark Drive File with type "sheet" and have client to interact with Lark Sheets API. +type Sheet struct { + *larksheet.Client + *larkdrive.File +} + +func NewSheet(client *larksheet.Client, file *larkdrive.File) (*Sheet, error) { + if file == nil { + return nil, errors.InvalidArgument(EntityLarkDrive, "file cannot be nil") + } + if file.Token == nil || *file.Token == "" { + return nil, errors.InvalidArgument(EntityLarkDrive, "file token cannot be empty") + } + if file.Name == nil || *file.Name == "" { + return nil, errors.InvalidArgument(EntityLarkDrive, "file name cannot be empty") + } + + if !IsLarkSheet(file) { + return nil, errors.InvalidArgument(EntityLarkDrive, "file is not a Lark Sheet") + } + + return &Sheet{ + Client: client, + File: file, + }, nil +} + +func IsLarkSheet(file *larkdrive.File) bool { + if file.Type == nil { + return false + } + return *file.Type == LarkFileTypeSheet +} + +// GenerateRevNumForDrive generates a revision number for a Lark Drive file based on its revisions. +// It sorts the revisions, converts them to a byte slice, and computes a CRC32 checksum. +// This is used to create a unique identifier for the file based on its revision history. +// If revisions ordered need to be preserved, we can copy the revisions slice before sorting it. +func GenerateRevNumForDrive(revisions []int) int { + if len(revisions) == 0 { + return 0 + } + + sort.Ints(revisions) + + table := crc64.MakeTable(crc64.ECMA) + var buf bytes.Buffer + for _, v := range revisions { + binary.Write(&buf, binary.BigEndian, uint32(v)) + } + + revNumber := int(crc64.Checksum(buf.Bytes(), table)) + buf.Reset() + + return revNumber +} diff --git a/ext/sheets/larkdrive/logger.go b/ext/sheets/larkdrive/logger.go new file mode 100644 index 0000000000..ed21644ae3 --- /dev/null +++ b/ext/sheets/larkdrive/logger.go @@ -0,0 +1,38 @@ +package larkdrive + +import ( + "context" + "fmt" + + "github.com/goto/salt/log" + + larkcore "github.com/larksuite/oapi-sdk-go/v3/core" +) + +type Logger struct { + log.Logger +} + +func (l *Logger) toMessage(i ...interface{}) string { + return fmt.Sprintf("%v", i...) +} + +func (l *Logger) Debug(_ context.Context, i ...interface{}) { + l.Logger.Debug(l.toMessage(i...)) +} + +func (l *Logger) Info(_ context.Context, i ...interface{}) { + l.Logger.Info(l.toMessage(i...)) +} + +func (l *Logger) Warn(_ context.Context, i ...interface{}) { + l.Logger.Warn(l.toMessage(i...)) +} + +func (l *Logger) Error(_ context.Context, i ...interface{}) { + l.Logger.Error(l.toMessage(i...)) +} + +func NewLogger(logger log.Logger) larkcore.Logger { + return &Logger{Logger: logger} +} diff --git a/ext/store/maxcompute/external_table.go b/ext/store/maxcompute/external_table.go index 4954c5f192..5335853f09 100644 --- a/ext/store/maxcompute/external_table.go +++ b/ext/store/maxcompute/external_table.go @@ -165,7 +165,7 @@ func (e ExternalTableHandle) enrichRoleToAssume(ctx context.Context, et *Externa func (e ExternalTableHandle) getLocation(ctx context.Context, et *ExternalTable, res *resource.Resource) (string, error) { switch et.Source.SourceType { - case GoogleSheet, GoogleDrive, LarkSheet: + case GoogleSheet, GoogleDrive, LarkSheet, LarkDrive: loc := et.Source.Location if loc == "" { tenantWithDetails, err := e.tenantDetailsGetter.GetDetails(ctx, res.Tenant()) diff --git a/ext/store/maxcompute/external_table_options.go b/ext/store/maxcompute/external_table_options.go index a3e08939a1..f1261db407 100644 --- a/ext/store/maxcompute/external_table_options.go +++ b/ext/store/maxcompute/external_table_options.go @@ -21,6 +21,7 @@ const ( GoogleSheet ExternalTableSourceType = "GOOGLE_SHEETS" GoogleDrive ExternalTableSourceType = "GOOGLE_DRIVE" LarkSheet ExternalTableSourceType = "LARK_SHEET" + LarkDrive ExternalTableSourceType = "LARK_DRIVE" OSS ExternalTableSourceType = "OSS" ) diff --git a/ext/store/maxcompute/external_table_spec.go b/ext/store/maxcompute/external_table_spec.go index 5e17762474..c9c34e5d7f 100644 --- a/ext/store/maxcompute/external_table_spec.go +++ b/ext/store/maxcompute/external_table_spec.go @@ -145,7 +145,7 @@ func (e ExternalSource) Validate() error { "Found `Source.Type` `OSS`. For `Source.Type` `OSS`, server only reads from `Source.Location` or default locations configured in Project Config, `ext_location`") } return nil - case LarkSheet: + case LarkSheet, LarkDrive: if len(e.SourceURIs) == 0 { return errors.InvalidArgument(EntityExternalTable, "source uri list is empty") } diff --git a/ext/store/maxcompute/lark_sync.go b/ext/store/maxcompute/lark_sync.go index 1d25dc6dd3..1f0fa5a27a 100644 --- a/ext/store/maxcompute/lark_sync.go +++ b/ext/store/maxcompute/lark_sync.go @@ -10,10 +10,15 @@ import ( "github.com/goto/optimus/core/resource" "github.com/goto/optimus/core/tenant" "github.com/goto/optimus/ext/sheets/lark" + "github.com/goto/optimus/ext/sheets/larkdrive" "github.com/goto/optimus/internal/errors" "github.com/goto/optimus/internal/lib/pool" ) +type LarkRevisionClient interface { + GetRevisionID(ctx context.Context, uri string) (int, error) +} + func (s *SyncerService) getLarkClient(ctx context.Context, tnnt tenant.Tenant) (*lark.Client, error) { secret, err := s.secretProvider.GetSecret(ctx, tnnt, LarkCredentialsKey) if err != nil { @@ -21,7 +26,19 @@ func (s *SyncerService) getLarkClient(ctx context.Context, tnnt tenant.Tenant) ( } client, err := lark.NewLarkClient(ctx, secret.Value()) if err != nil { - return nil, fmt.Errorf("not able to create Lark Client err: %w", err) + return nil, fmt.Errorf("not able to create LarkSheet Client err: %w", err) + } + return client, nil +} + +func (s *SyncerService) getLarkDriveClient(ctx context.Context, tnnt tenant.Tenant) (*larkdrive.Client, error) { + secret, err := s.secretProvider.GetSecret(ctx, tnnt, LarkCredentialsKey) + if err != nil { + return nil, err + } + client, err := larkdrive.NewClient(secret.Value(), s.logger) + if err != nil { + return nil, fmt.Errorf("not able to create LarkSheet Client err: %w", err) } return client, nil } @@ -32,20 +49,31 @@ func (s *SyncerService) getLarkRevisionIDs(ctx context.Context, tnnt tenant.Tena if err != nil { return nil, err } + ld, err := s.getLarkDriveClient(ctx, tnnt) + if err != nil { + return nil, err + } var jobs []func() pool.JobResult[resource.SourceModifiedRevisionStatus] for _, et := range ets { et := et - if et.Source.SourceType != LarkSheet { + + var client LarkRevisionClient + switch et.Source.SourceType { + case LarkSheet: + client = lc + case LarkDrive: + client = ld + default: response = append(response, resource.SourceModifiedRevisionStatus{ FullName: et.FullName(), - Err: errors.InvalidArgument(EntityExternalTable, "source is not LarkSheet"), + Err: errors.InvalidArgument(EntityExternalTable, "source is not LarkSheet or LarkDrive"), }) continue } jobs = append(jobs, func() pool.JobResult[resource.SourceModifiedRevisionStatus] { - revisionID, err := lc.GetRevisionID(ctx, et.Source.SourceURIs[0]) + revisionID, err := client.GetRevisionID(ctx, et.Source.SourceURIs[0]) if err != nil { return pool.JobResult[resource.SourceModifiedRevisionStatus]{ Output: resource.SourceModifiedRevisionStatus{ @@ -125,3 +153,64 @@ func processLarkSheet(ctx context.Context, lark *lark.Client, ossClient *oss.Cli } return revisionNumber, writeToBucket(ctx, ossClient, bucketName, objectKey, content) } + +func processLarkDrive(ctx context.Context, drive *larkdrive.Client, ossClient *oss.Client, et *ExternalTable, commonLocation string) (int, error) { + var revisionNumber int + + if len(et.Source.SourceURIs) == 0 { + return 0, errors.InvalidArgument(EntityExternalTable, "source URI is empty for LarkDrive") + } + + folderToken, err := drive.GetFolderToken(et.Source.SourceURIs[0]) + if err != nil { + return revisionNumber, err + } + + bucketName, objectPath, err := getBucketNameAndPath(commonLocation, et.Source.Location, et.FullName()) + if err != nil { + return revisionNumber, err + } + + err = deleteFolderFromBucket(ctx, ossClient, bucketName, objectPath) + if err != nil { + return revisionNumber, err + } + + var revisionNumbers []int + err = drive.IterateSheet(ctx, folderToken, func(sheet *larkdrive.Sheet) error { + sheetRevisionNum, content, err := getLSheetContent(ctx, et, sheet.Client) + if err != nil { + return err + } + + revisionNumbers = append(revisionNumbers, sheetRevisionNum) + objectKey := objectPath + *sheet.Name + ".csv" + + if err = writeToBucket(ctx, ossClient, bucketName, objectKey, content); err != nil { + return errors.AddErrContext(err, EntityExternalTable, fmt.Sprintf("failed to write sheet %s to bucket", *sheet.Name)) + } + + return nil + }) + if err != nil { + return revisionNumber, errors.AddErrContext(err, EntityExternalTable, "failed to iterate sheets in Lark Drive folder") + } + + revisionNumber = larkdrive.GenerateRevNumForDrive(revisionNumbers) + + return revisionNumber, nil +} + +func processLarkTypeSources(ctx context.Context, externalTableClients ExtTableClients, et *ExternalTable, commonLocation string) (int, error) { + sheetClient := externalTableClients.LarkSheet + driveClient := externalTableClients.LarkDrive + ossClient := externalTableClients.OSS + + switch et.Source.SourceType { + case LarkSheet: + return processLarkSheet(ctx, sheetClient, ossClient, et, commonLocation) + case LarkDrive: + return processLarkDrive(ctx, driveClient, ossClient, et, commonLocation) + } + return 0, errors.InvalidArgument(EntityExternalTable, fmt.Sprintf("unsupported source type %s for Lark", et.Source.SourceType)) +} diff --git a/ext/store/maxcompute/sheet_sync.go b/ext/store/maxcompute/sheet_sync.go index a73c77c0fb..57cfab3e8a 100644 --- a/ext/store/maxcompute/sheet_sync.go +++ b/ext/store/maxcompute/sheet_sync.go @@ -16,6 +16,7 @@ import ( "github.com/goto/optimus/ext/sheets/gdrive" "github.com/goto/optimus/ext/sheets/gsheet" "github.com/goto/optimus/ext/sheets/lark" + "github.com/goto/optimus/ext/sheets/larkdrive" "github.com/goto/optimus/internal/errors" "github.com/goto/optimus/internal/lib/pool" ) @@ -279,7 +280,7 @@ func (s *SyncerService) GetExternalTablesDueForSync(ctx context.Context, tnnt te if err != nil { return nil, nil, err } - case LarkSheet: + case LarkSheet, LarkDrive: toUpdateET, unModifiedET, err = s.getLarkExternalTablesDueForSync(ctx, tnnt, externalTables, lastUpdateMap) if err != nil { return nil, nil, err @@ -397,10 +398,11 @@ func (s *SyncerService) Sync(ctx context.Context, res *resource.Resource) error } type ExtTableClients struct { - GSheet *gsheet.GSheets - OSS *oss.Client - GDrive *gdrive.GDrive - Lark *lark.Client + GSheet *gsheet.GSheets + OSS *oss.Client + GDrive *gdrive.GDrive + LarkSheet *lark.Client + LarkDrive *larkdrive.Client } func (s *SyncerService) getExtTableClients(ctx context.Context, tnnt tenant.Tenant, sourceTypes ExternalTableSources) (ExtTableClients, error) { @@ -429,12 +431,26 @@ func (s *SyncerService) getExtTableClients(ctx context.Context, tnnt tenant.Tena } clients.GDrive = driveSrv } - if sourceTypes.Has(LarkSheet) { + + if sourceTypes.Has(LarkDrive) { + larkDriveClient, err := s.getLarkDriveClient(ctx, tnnt) + if err != nil { + return clients, err + } + clients.LarkDrive = larkDriveClient + + larkClient, err := s.getLarkClient(ctx, tnnt) + if err != nil { + return clients, err + } + clients.LarkSheet = larkClient + } + if sourceTypes.Has(LarkSheet) && clients.LarkSheet == nil { larkClient, err := s.getLarkClient(ctx, tnnt) if err != nil { return clients, err } - clients.Lark = larkClient + clients.LarkSheet = larkClient } creds, err := s.secretProvider.GetSecret(ctx, tnnt, OSSCredsKey) @@ -505,7 +521,7 @@ func processResource(ctx context.Context, syncRepo SyncRepo, externalTableClient } return err case LarkSheet: - revisionNumber, err := processLarkSheet(ctx, externalTableClients.Lark, externalTableClients.OSS, et, commonLocation) + revisionNumber, err := processLarkTypeSources(ctx, externalTableClients, et, commonLocation) syncStatusRemarks := map[string]string{} if err != nil { syncStatusRemarks["error"] = err.Error() diff --git a/go.mod b/go.mod index 058f87ad4b..feae1562e4 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/gtank/cryptopasta v0.0.0-20170601214702-1f550f6f2f69 github.com/jackc/pgx/v5 v5.2.0 github.com/kushsharma/parallel v0.2.1 + github.com/larksuite/oapi-sdk-go/v3 v3.4.20 github.com/lib/pq v1.10.4 github.com/mattn/go-isatty v0.0.16 github.com/mitchellh/mapstructure v1.4.3 @@ -101,7 +102,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect github.com/googleapis/gax-go/v2 v2.7.0 // indirect github.com/gorilla/css v1.0.0 // indirect - github.com/gorilla/websocket v1.4.2 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect diff --git a/go.sum b/go.sum index 3c935bf75f..13eb0b8aa1 100644 --- a/go.sum +++ b/go.sum @@ -883,8 +883,9 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7 github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/goto/salt v0.3.0 h1:7bFVqh6/zHfyImkrl2mYJHsFuGd2OBM/8rKs+giDpbc= github.com/goto/salt v0.3.0/go.mod h1:L8PtSbpUFFo/Yh5YZ0FTgV9FIAvtqGdNa2BkA5M3jWo= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= @@ -1093,6 +1094,8 @@ github.com/kushsharma/parallel v0.2.1 h1:y9LgTLrtKBWt/YyKE8DYrUxKupiZTriWYKXB+hy github.com/kushsharma/parallel v0.2.1/go.mod h1:6JCy2+DRCUfZ0VFBUg6HG8IdDTDKuVL02dhvjUc+xt8= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/larksuite/oapi-sdk-go/v3 v3.4.20 h1:Ul1NWAHXYzbXBHFmUxMTSZ9v2ahy/O8EthYOQnLvPo0= +github.com/larksuite/oapi-sdk-go/v3 v3.4.20/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= diff --git a/internal/errors/errors.go b/internal/errors/errors.go index 207d9516eb..54624606d6 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -21,6 +21,7 @@ const ( ErrAlreadyExists ErrorType = "Resource Already Exists" ErrInvalidArgument ErrorType = "Invalid Argument" ErrFailedPrecond ErrorType = "Failed Precondition" + ErrForbidden ErrorType = "Forbidden" ErrInvalidState ErrorType = "Invalid State" ) @@ -38,7 +39,10 @@ func (*DomainError) Is(tgt error) bool { } func AddErrContext(err error, entity, msg string) *DomainError { - errType := ErrInternalError + return AddErrContextWithType(err, ErrInternalError, entity, msg) +} + +func AddErrContextWithType(err error, errType ErrorType, entity, msg string) *DomainError { var de *DomainError if errors.As(err, &de) { errType = de.ErrorType @@ -125,6 +129,15 @@ func FailedPrecondition(entity, msg string) *DomainError { } } +func NewForbidden(entity, msg string) *DomainError { + return &DomainError{ + ErrorType: ErrForbidden, + Entity: entity, + Message: msg, + WrappedErr: nil, + } +} + func Is(err, target error) bool { return errors.Is(err, target) } diff --git a/internal/errors/errors_test.go b/internal/errors/errors_test.go index 184bf77d52..067387817c 100644 --- a/internal/errors/errors_test.go +++ b/internal/errors/errors_test.go @@ -50,6 +50,14 @@ func TestErrors(t *testing.T) { assert.Error(t, invalidStateTransition) assert.ErrorContains(t, invalidStateTransition, "transition is invalid") }) + t.Run("creates a forbidden error", func(t *testing.T) { + forbiddenError := errors.NewForbidden(testEntity, "access is forbidden") + + assert.Error(t, forbiddenError) + assert.ErrorContains(t, forbiddenError, "access is forbidden") + assert.True(t, errors.IsErrorType(forbiddenError, errors.ErrForbidden)) + assert.False(t, errors.IsErrorType(forbiddenError, errors.ErrInternalError)) + }) t.Run("creates a domain error", func(t *testing.T) { domainError := errors.NewError(errors.ErrFailedPrecond, testEntity, "random error")