Skip to content

Commit

Permalink
Add API v2 (#377)
Browse files Browse the repository at this point in the history
* Convert summary in document model to a pointer so we can delete the value

* Add method and path to successful patch log lines

* Add document file revisions to data model

* Use assert.ElementsMatch instead of reflect.DeepEqual so order is ignored for slices

* Add server package

* Add API v2

* Enable using v2 of the API from the frontend via the 'api_v2' feature flag

* Use api_version from the config service

* Add api_version for new fetch call

* Add api_version for another fetch
  • Loading branch information
jfreda authored Oct 23, 2023
1 parent b00e6a2 commit d875bc7
Show file tree
Hide file tree
Showing 46 changed files with 7,608 additions and 143 deletions.
8 changes: 8 additions & 0 deletions configs/config.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ email {
from_address = "[email protected]"
}

// FeatureFlags contain available feature flags.
feature_flags {
// api_v2 enables v2 of the API.
flag "api_v2" {
enabled = false
}
}

// google_workspace configures Hermes to work with Google Workspace.
google_workspace {
// create_doc_shortcuts enables creating a shortcut in the shortcuts_folder
Expand Down
8 changes: 6 additions & 2 deletions internal/api/documents.go
Original file line number Diff line number Diff line change
Expand Up @@ -717,7 +717,7 @@ Hermes

// Summary.
if req.Summary != nil {
model.Summary = *req.Summary
model.Summary = req.Summary
}

// Title.
Expand All @@ -739,7 +739,11 @@ Hermes
}

w.WriteHeader(http.StatusOK)
l.Info("patched document", "doc_id", docID)
l.Info("patched document",
"method", r.Method,
"path", r.URL.Path,
"doc_id", docID,
)

// Compare Algolia and database documents to find data inconsistencies.
// Get document object from Algolia.
Expand Down
10 changes: 7 additions & 3 deletions internal/api/drafts.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ func DraftsHandler(
Name: req.Product,
},
Status: models.WIPDocumentStatus,
Summary: req.Summary,
Summary: &req.Summary,
Title: req.Title,
}
if err := model.Create(db); err != nil {
Expand Down Expand Up @@ -1104,7 +1104,7 @@ func DraftsDocumentHandler(
// Summary.
if req.Summary != nil {
doc.Summary = *req.Summary
model.Summary = *req.Summary
model.Summary = req.Summary
}

// Title.
Expand Down Expand Up @@ -1176,7 +1176,11 @@ func DraftsDocumentHandler(
fmt.Sprintf("[%s] %s", doc.DocNumber, doc.Title))

w.WriteHeader(http.StatusOK)
l.Info("patched draft document", "doc_id", docId)
l.Info("patched draft document",
"method", r.Method,
"path", r.URL.Path,
"doc_id", docId,
)

// Compare Algolia and database documents to find data inconsistencies.
// Get document object from Algolia.
Expand Down
63 changes: 55 additions & 8 deletions internal/api/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
"github.com/iancoleman/strcase"
"github.com/stretchr/testify/assert"
)

// contains returns true if a string is present in a slice of strings.
Expand Down Expand Up @@ -115,6 +116,12 @@ func respondError(
http.Error(w, userErrMsg, httpCode)
}

// fakeT fulfills the assert.TestingT interface so we can use
// assert.ElementsMatch.
type fakeT struct{}

func (t fakeT) Errorf(string, ...interface{}) {}

// compareAlgoliaAndDatabaseDocument compares data for a document stored in
// Algolia and the database to determine any inconsistencies, which are returned
// back as a (multierror) error.
Expand Down Expand Up @@ -224,7 +231,7 @@ func compareAlgoliaAndDatabaseDocument(
dbApprovedBy = append(dbApprovedBy, r.User.EmailAddress)
}
}
if !reflect.DeepEqual(algoApprovedBy, dbApprovedBy) {
if !assert.ElementsMatch(fakeT{}, algoApprovedBy, dbApprovedBy) {
result = multierror.Append(result,
fmt.Errorf(
"approvedBy not equal, algolia=%v, db=%v",
Expand All @@ -242,7 +249,7 @@ func compareAlgoliaAndDatabaseDocument(
for _, a := range dbDoc.Approvers {
dbApprovers = append(dbApprovers, a.EmailAddress)
}
if !reflect.DeepEqual(algoApprovers, dbApprovers) {
if !assert.ElementsMatch(fakeT{}, algoApprovers, dbApprovers) {
result = multierror.Append(result,
fmt.Errorf(
"approvers not equal, algolia=%v, db=%v",
Expand All @@ -263,7 +270,7 @@ func compareAlgoliaAndDatabaseDocument(
dbChangesRequestedBy = append(dbChangesRequestedBy, r.User.EmailAddress)
}
}
if !reflect.DeepEqual(algoChangesRequestedBy, dbChangesRequestedBy) {
if !assert.ElementsMatch(fakeT{}, algoChangesRequestedBy, dbChangesRequestedBy) {
result = multierror.Append(result,
fmt.Errorf(
"changesRequestedBy not equal, algolia=%v, db=%v",
Expand All @@ -281,7 +288,7 @@ func compareAlgoliaAndDatabaseDocument(
for _, c := range dbDoc.Contributors {
dbContributors = append(dbContributors, c.EmailAddress)
}
if !reflect.DeepEqual(algoContributors, dbContributors) {
if !assert.ElementsMatch(fakeT{}, algoContributors, dbContributors) {
result = multierror.Append(result,
fmt.Errorf(
"contributors not equal, algolia=%v, db=%v",
Expand Down Expand Up @@ -353,7 +360,7 @@ func compareAlgoliaAndDatabaseDocument(
)
}

if !reflect.DeepEqual(algoCFVal, dbCFVal) {
if !assert.ElementsMatch(fakeT{}, algoCFVal, dbCFVal) {
result = multierror.Append(result,
fmt.Errorf(
"custom field %s not equal, algolia=%v, db=%v",
Expand All @@ -379,8 +386,24 @@ func compareAlgoliaAndDatabaseDocument(
"doc type %q not found", algoDocType))
}

// Compare file revisions.
// TODO: need to store this in the database first.
// Compare fileRevisions.
algoFileRevisions, err := getMapStringStringValue(algoDoc, "fileRevisions")
if err != nil {
result = multierror.Append(
result, fmt.Errorf("error getting fileRevisions value: %w", err))
} else {
dbFileRevisions := make(map[string]string)
for _, fr := range dbDoc.FileRevisions {
dbFileRevisions[fr.GoogleDriveFileRevisionID] = fr.Name
}
if !reflect.DeepEqual(algoFileRevisions, dbFileRevisions) {
result = multierror.Append(result,
fmt.Errorf(
"fileRevisions not equal, algolia=%v, db=%v",
algoFileRevisions, dbFileRevisions),
)
}
}

// Compare modifiedTime.
algoModifiedTime, err := getInt64Value(algoDoc, "modifiedTime")
Expand Down Expand Up @@ -473,7 +496,7 @@ func compareAlgoliaAndDatabaseDocument(
result, fmt.Errorf("error getting summary value: %w", err))
} else {
dbSummary := dbDoc.Summary
if algoSummary != dbSummary {
if dbSummary != nil && algoSummary != *dbSummary {
result = multierror.Append(result,
fmt.Errorf(
"summary not equal, algolia=%v, db=%v",
Expand Down Expand Up @@ -517,6 +540,30 @@ func getInt64Value(in map[string]any, key string) (int64, error) {
return result, nil
}

func getMapStringStringValue(in map[string]any, key string) (
map[string]string, error,
) {
result := make(map[string]string)

if v, ok := in[key]; ok {
if reflect.TypeOf(v).Kind() == reflect.Map {
for vk, vv := range v.(map[string]any) {
if vv, ok := vv.(string); ok {
result[vk] = vv
} else {
return nil, fmt.Errorf(
"invalid type: map value element is not a string")
}
}
return result, nil
} else {
return nil, fmt.Errorf("invalid type: value is not a map")
}
}

return result, nil
}

func getStringValue(in map[string]any, key string) (string, error) {
var result string

Expand Down
136 changes: 134 additions & 2 deletions internal/api/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ func TestCompareAlgoliaAndDatabaseDocument(t *testing.T) {
"createdTime": float64(time.Date(
2023, time.April, 5, 1, 0, 0, 0, time.UTC).Unix()),
"currentVersion": "1.2.3",
"fileRevisions": map[string]any{
"1": "FileRevision1",
"2": "FileRevision2",
},
"modifiedTime": float64(time.Date(
2023, time.April, 5, 23, 0, 0, 0, time.UTC).Unix()),
"owners": []any{
Expand Down Expand Up @@ -204,10 +208,20 @@ func TestCompareAlgoliaAndDatabaseDocument(t *testing.T) {
2023, time.April, 5, 1, 0, 0, 0, time.UTC),
DocumentModifiedAt: time.Date(
2023, time.April, 5, 23, 0, 0, 0, time.UTC),
FileRevisions: []models.DocumentFileRevision{
{
GoogleDriveFileRevisionID: "1",
Name: "FileRevision1",
},
{
GoogleDriveFileRevisionID: "2",
Name: "FileRevision2",
},
},
Owner: &models.User{
EmailAddress: "[email protected]",
},
Summary: "Summary1",
Summary: &[]string{"Summary1"}[0],
Status: models.InReviewDocumentStatus,
},
dbDocReviews: models.DocumentReviews{
Expand Down Expand Up @@ -270,6 +284,124 @@ func TestCompareAlgoliaAndDatabaseDocument(t *testing.T) {
},
},

"good with different order of slice and map fields": {
algoDoc: map[string]any{
"appCreated": true,
"approvedBy": []any{
"[email protected]",
"[email protected]",
},
"approvers": []any{
"[email protected]",
"[email protected]",
},
"changesRequestedBy": []any{
"[email protected]",
"[email protected]",
},
"contributors": []any{
"[email protected]",
"[email protected]",
},
"createdTime": float64(time.Date(
2023, time.April, 5, 1, 0, 0, 0, time.UTC).Unix()),
"docNumber": "ABC-123",
"docType": "RFC",
"fileRevisions": map[string]any{
"2": "FileRevision2",
"1": "FileRevision1",
},
"modifiedTime": float64(time.Date(
2023, time.April, 5, 23, 0, 0, 0, time.UTC).Unix()),
"owners": []any{"[email protected]"},
"product": "Product1",
"stakeholders": []any{
"[email protected]",
"[email protected]",
},
},
dbDoc: models.Document{
DocumentNumber: 123,
DocumentType: models.DocumentType{
Name: "RFC",
},
Product: models.Product{
Name: "Product1",
Abbreviation: "ABC",
},
Approvers: []*models.User{
{
EmailAddress: "[email protected]",
},
{
EmailAddress: "[email protected]",
},
},
Contributors: []*models.User{
{
EmailAddress: "[email protected]",
},
{
EmailAddress: "[email protected]",
},
},
CustomFields: []*models.DocumentCustomField{
{
DocumentTypeCustomField: models.DocumentTypeCustomField{
Name: "Stakeholders",
DocumentType: models.DocumentType{
Name: "RFC",
},
},
Value: `["[email protected]","[email protected]"]`,
},
},
DocumentCreatedAt: time.Date(
2023, time.April, 5, 1, 0, 0, 0, time.UTC),
DocumentModifiedAt: time.Date(
2023, time.April, 5, 23, 0, 0, 0, time.UTC),
FileRevisions: []models.DocumentFileRevision{
{
GoogleDriveFileRevisionID: "1",
Name: "FileRevision1",
},
{
GoogleDriveFileRevisionID: "2",
Name: "FileRevision2",
},
},
Owner: &models.User{
EmailAddress: "[email protected]",
},
},
dbDocReviews: models.DocumentReviews{
{
Status: models.ApprovedDocumentReviewStatus,
User: models.User{
EmailAddress: "[email protected]",
},
},
{
Status: models.ApprovedDocumentReviewStatus,
User: models.User{
EmailAddress: "[email protected]",
},
},
{
Status: models.ChangesRequestedDocumentReviewStatus,
User: models.User{
EmailAddress: "[email protected]",
},
},
{
Status: models.ChangesRequestedDocumentReviewStatus,
User: models.User{
EmailAddress: "[email protected]",
},
},
},
},

"bad objectID": {
algoDoc: map[string]any{
"objectID": "GoogleFileID1",
Expand Down Expand Up @@ -839,7 +971,7 @@ func TestCompareAlgoliaAndDatabaseDocument(t *testing.T) {
Owner: &models.User{
EmailAddress: "[email protected]",
},
Summary: "BadSummary1",
Summary: &[]string{"BadSummary1"}[0],
},
shouldErr: true,
errContains: "summary not equal",
Expand Down
Loading

0 comments on commit d875bc7

Please sign in to comment.