diff --git a/internal/datasources/service/service.go b/internal/datasources/service/service.go index cdc4c0bd6b..e13ca0339c 100644 --- a/internal/datasources/service/service.go +++ b/internal/datasources/service/service.go @@ -11,6 +11,7 @@ import ( "fmt" "github.com/google/uuid" + "github.com/rs/zerolog" "google.golang.org/grpc/codes" "google.golang.org/protobuf/encoding/protojson" @@ -187,7 +188,7 @@ func (d *dataSourceService) Create( defer func(stx serviceTX) { err := stx.Rollback() if err != nil { - fmt.Printf("failed to rollback transaction: %v", err) + zerolog.Ctx(ctx).Error().Err(err).Msg("failed to rollback transaction") } }(stx) @@ -262,11 +263,61 @@ func (d *dataSourceService) Update( panic("implement me") } -// nolint:revive // there is a TODO +// Delete deletes a data source in the given project. func (d *dataSourceService) Delete( ctx context.Context, id uuid.UUID, project uuid.UUID, opts *Options) error { - //TODO implement me - panic("implement me") + stx, err := d.txBuilder(d, opts) + if err != nil { + return fmt.Errorf("failed to start transaction: %w", err) + } + defer func(stx serviceTX) { + err := stx.Rollback() + if err != nil { + zerolog.Ctx(ctx).Error().Err(err).Msg("failed to rollback transaction") + } + }(stx) + + // Get the transaction querier + tx := stx.Q() + + // List rule types referencing the data source + ret, err := tx.ListRuleTypesReferencesByDataSource(ctx, db.ListRuleTypesReferencesByDataSourceParams{ + DataSourcesID: id, + ProjectID: project, + }) + if err != nil { + return fmt.Errorf("failed to list rule types referencing data source %s: %w", id, err) + } + + // Check if the data source is in use by any rule types + if len(ret) > 0 { + // Return an error with the rule types that are using the data source + var existingRefs []string + for _, r := range ret { + existingRefs = append(existingRefs, r.RuleTypeID.String()) + } + return util.UserVisibleError(codes.FailedPrecondition, + "data source %s is in use by the following rule types: %v", id, existingRefs) + } + + // Delete the data source record + _, err = tx.DeleteDataSource(ctx, db.DeleteDataSourceParams{ + ID: id, + ProjectID: project, + }) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return util.UserVisibleError(codes.NotFound, + "data source with id %s not found in project %s", id, project) + } + return fmt.Errorf("failed to delete data source with id %s: %w", id, err) + } + + // Commit the transaction + if err := stx.Commit(); err != nil { + return fmt.Errorf("failed to commit transaction: %w", err) + } + return nil } // nolint:revive // there is a TODO diff --git a/internal/datasources/service/service_test.go b/internal/datasources/service/service_test.go index d0f9ed4f8d..61d3be41ea 100644 --- a/internal/datasources/service/service_test.go +++ b/internal/datasources/service/service_test.go @@ -877,3 +877,167 @@ func restDriverToJson(t *testing.T, rs *minderv1.RestDataSource_Def) []byte { return out } + +func TestDelete(t *testing.T) { + t.Parallel() + + type args struct { + id uuid.UUID + project uuid.UUID + opts *Options + } + + tests := []struct { + name string + args args + setup func(args args, mockDB *mockdb.MockStore) + wantErr bool + }{ + { + name: "Successful deletion", + args: args{ + id: uuid.New(), + project: uuid.New(), + opts: &Options{}, + }, + setup: func(args args, mockDB *mockdb.MockStore) { + // Mock ListRuleTypesReferencesByDataSource to return empty list + mockDB.EXPECT(). + ListRuleTypesReferencesByDataSource(gomock.Any(), gomock.Eq(db.ListRuleTypesReferencesByDataSourceParams{ + DataSourcesID: args.id, + ProjectID: args.project, + })). + Return([]db.RuleTypeDataSource{}, nil) + + // Mock DeleteDataSource to succeed + mockDB.EXPECT(). + DeleteDataSource(gomock.Any(), gomock.Eq(db.DeleteDataSourceParams{ + ID: args.id, + ProjectID: args.project, + })). + Return(db.DataSource{}, nil) + }, + wantErr: false, + }, + { + name: "Data source not found", + args: args{ + id: uuid.New(), + project: uuid.New(), + opts: &Options{}, + }, + setup: func(args args, mockDB *mockdb.MockStore) { + // Mock ListRuleTypesReferencesByDataSource to return empty list + mockDB.EXPECT(). + ListRuleTypesReferencesByDataSource(gomock.Any(), gomock.Eq(db.ListRuleTypesReferencesByDataSourceParams{ + DataSourcesID: args.id, + ProjectID: args.project, + })). + Return([]db.RuleTypeDataSource{}, nil) + + // Mock DeleteDataSource to return sql.ErrNoRows + mockDB.EXPECT(). + DeleteDataSource(gomock.Any(), gomock.Eq(db.DeleteDataSourceParams{ + ID: args.id, + ProjectID: args.project, + })). + Return(db.DataSource{}, sql.ErrNoRows) + }, + wantErr: true, + }, + { + name: "Data source is in use", + args: args{ + id: uuid.New(), + project: uuid.New(), + opts: &Options{}, + }, + setup: func(args args, mockDB *mockdb.MockStore) { + // Mock ListRuleTypesReferencesByDataSource to return non-empty list + mockDB.EXPECT(). + ListRuleTypesReferencesByDataSource(gomock.Any(), gomock.Eq(db.ListRuleTypesReferencesByDataSourceParams{ + DataSourcesID: args.id, + ProjectID: args.project, + })). + Return([]db.RuleTypeDataSource{ + {RuleTypeID: uuid.New()}, + }, nil) + }, + wantErr: true, + }, + { + name: "Database error when listing references", + args: args{ + id: uuid.New(), + project: uuid.New(), + opts: &Options{}, + }, + setup: func(args args, mockDB *mockdb.MockStore) { + // Mock ListRuleTypesReferencesByDataSource to return an error + mockDB.EXPECT(). + ListRuleTypesReferencesByDataSource(gomock.Any(), gomock.Eq(db.ListRuleTypesReferencesByDataSourceParams{ + DataSourcesID: args.id, + ProjectID: args.project, + })). + Return(nil, fmt.Errorf("database error")) + }, + wantErr: true, + }, + { + name: "Database error when deleting data source", + args: args{ + id: uuid.New(), + project: uuid.New(), + opts: &Options{}, + }, + setup: func(args args, mockDB *mockdb.MockStore) { + // Mock ListRuleTypesReferencesByDataSource to return empty list + mockDB.EXPECT(). + ListRuleTypesReferencesByDataSource(gomock.Any(), gomock.Eq(db.ListRuleTypesReferencesByDataSourceParams{ + DataSourcesID: args.id, + ProjectID: args.project, + })). + Return([]db.RuleTypeDataSource{}, nil) + + // Mock DeleteDataSource to return an error + mockDB.EXPECT(). + DeleteDataSource(gomock.Any(), gomock.Eq(db.DeleteDataSourceParams{ + ID: args.id, + ProjectID: args.project, + })). + Return(db.DataSource{}, fmt.Errorf("database error")) + }, + wantErr: true, + }, + } + + for _, tt := range tests { + tt := tt // capture range variable + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Setup + mockStore := mockdb.NewMockStore(ctrl) + + svc := NewDataSourceService(mockStore) + svc.txBuilder = func(_ *dataSourceService, _ txGetter) (serviceTX, error) { + return &fakeTxBuilder{ + store: mockStore, + }, nil + } + + tt.setup(tt.args, mockStore) + + err := svc.Delete(context.Background(), tt.args.id, tt.args.project, tt.args.opts) + if tt.wantErr { + assert.Error(t, err) + return + } + + require.NoError(t, err) + }) + } +}