Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions providers/livestorm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Livestorm connector

This directory contains the deep connector implementation for Livestorm.

- `connector.go` wires the metadata provider.
- `metadata/schemas.json` contains static object metadata.
26 changes: 26 additions & 0 deletions providers/livestorm/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package livestorm
import (
"github.com/amp-labs/connectors/common"
"github.com/amp-labs/connectors/internal/components"
"github.com/amp-labs/connectors/internal/components/deleter"
"github.com/amp-labs/connectors/internal/components/operations"
"github.com/amp-labs/connectors/internal/components/reader"
"github.com/amp-labs/connectors/internal/components/schema"
"github.com/amp-labs/connectors/internal/components/writer"
"github.com/amp-labs/connectors/providers"
"github.com/amp-labs/connectors/providers/livestorm/metadata"
)
Expand All @@ -16,6 +18,8 @@ type Connector struct {
common.RequireAuthenticatedClient
components.SchemaProvider
components.Reader
components.Writer
components.Deleter
}

func NewConnector(params common.ConnectorParams) (*Connector, error) {
Expand All @@ -41,5 +45,27 @@ func constructor(base *components.Connector) (*Connector, error) {
},
)

connector.Writer = writer.NewHTTPWriter(
connector.HTTPClient().Client,
components.NewEmptyEndpointRegistry(),
connector.ProviderContext.Module(),
operations.WriteHandlers{
BuildRequest: connector.buildWriteRequest,
ParseResponse: connector.parseWriteResponse,
ErrorHandler: common.InterpretError,
},
)

connector.Deleter = deleter.NewHTTPDeleter(
connector.HTTPClient().Client,
components.NewEmptyEndpointRegistry(),
connector.ProviderContext.Module(),
operations.DeleteHandlers{
BuildRequest: connector.buildDeleteRequest,
ParseResponse: connector.parseDeleteResponse,
ErrorHandler: common.InterpretError,
},
)

return connector, nil
}
49 changes: 49 additions & 0 deletions providers/livestorm/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package livestorm

import (
"context"
"fmt"
"net/http"

"github.com/amp-labs/connectors/common"
"github.com/amp-labs/connectors/common/urlbuilder"
"github.com/amp-labs/connectors/internal/httpkit"
)

// Delete an event: https://developers.livestorm.co/reference/delete_events-id
func (c *Connector) buildDeleteRequest(ctx context.Context, params common.DeleteParams) (*http.Request, error) {
if err := params.ValidateParams(); err != nil {
return nil, err
}

if params.ObjectName != objectEvents {
return nil, common.ErrOperationNotSupportedForObject
}

u, err := urlbuilder.New(c.ProviderInfo().BaseURL, apiVersion, objectEvents, params.RecordId)
if err != nil {
return nil, err
}

req, err := http.NewRequestWithContext(ctx, http.MethodDelete, u.String(), nil)
if err != nil {
return nil, err
}

req.Header.Set("Accept", jsonAPIContentType)

return req, nil
}

func (c *Connector) parseDeleteResponse(
_ context.Context,
_ common.DeleteParams,
_ *http.Request,
response *common.JSONHTTPResponse,
) (*common.DeleteResult, error) {
if !httpkit.Status2xx(response.Code) {
return nil, fmt.Errorf("%w: failed to delete record: %d", common.ErrRequestFailed, response.Code)
}

return &common.DeleteResult{Success: true}, nil
}
64 changes: 64 additions & 0 deletions providers/livestorm/delete_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package livestorm

import (
"net/http"
"testing"

"github.com/amp-labs/connectors"
"github.com/amp-labs/connectors/common"
"github.com/amp-labs/connectors/test/utils/mockutils/mockcond"
"github.com/amp-labs/connectors/test/utils/mockutils/mockserver"
"github.com/amp-labs/connectors/test/utils/testroutines"
)

func TestDelete(t *testing.T) {
t.Parallel()

tests := []testroutines.Delete{
{
Name: "Object name is required",
Server: mockserver.Dummy(),
ExpectedErrs: []error{common.ErrMissingObjects},
},
{
Name: "Record id is required",
Input: common.DeleteParams{ObjectName: objectEvents},
Server: mockserver.Dummy(),
ExpectedErrs: []error{common.ErrMissingRecordID},
},
{
Name: "Unsupported object",
Input: common.DeleteParams{ObjectName: "people", RecordId: "p1"},
Server: mockserver.Dummy(),
ExpectedErrs: []error{common.ErrOperationNotSupportedForObject},
},
{
Name: "Delete event (204)",
Input: common.DeleteParams{
ObjectName: objectEvents,
RecordId: "evt_del",
},
Server: mockserver.Conditional{
Setup: mockserver.ContentJSON(),
If: mockcond.And{
mockcond.MethodDELETE(),
mockcond.Path("/v1/events/evt_del"),
},
Then: mockserver.Response(http.StatusNoContent, nil),
}.Server(),
Expected: &common.DeleteResult{Success: true},
},
}

for _, tt := range tests {
tt := tt

t.Run(tt.Name, func(t *testing.T) {
t.Parallel()

tt.Run(t, func() (connectors.DeleteConnector, error) {
return constructTestConnector(tt.Server.URL)
})
})
}
}
3 changes: 0 additions & 3 deletions providers/livestorm/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,4 @@ import "errors"
var (
// ErrSessionIDRequired is returned when reading session_chat_messages without a session id in ReadParams.Filter.
ErrSessionIDRequired = errors.New("read session_chat_messages requires a non-empty ReadParams.Filter (session id)")

// ErrJobIDRequired is returned when reading jobs without a job id in ReadParams.Filter.
ErrJobIDRequired = errors.New("read jobs requires a non-empty ReadParams.Filter (job id)")
)
6 changes: 2 additions & 4 deletions providers/livestorm/jsonapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ import (
)

// extractJSONAPIResourceNodes returns JSON:API primary `data` resource nodes only (no flattening).
// List endpoints use `data` as an array (the usual Read path). Livestorm also returns `data` as a
// single resource object for GET /v1/jobs/{id}; that yields one row. If Read is narrowed to
// collections-only later, drop jobs (or similar singleton reads) from read routing instead of
// changing this extractor.
// List read responses use `data` as an array of resource objects. JSON:API also allows `data` as a
// single resource object; that is normalized to a one-element slice so parsing stays spec-tolerant.
func extractJSONAPIResourceNodes(root *ajson.Node) ([]*ajson.Node, error) {
items, arrErr := jsonquery.New(root).ArrayOptional("data")
if arrErr == nil {
Expand Down
3 changes: 2 additions & 1 deletion providers/livestorm/metadata/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ The static file `schemas.json` is embedded by `metadata.go` and defines metadata
- [List events](https://developers.livestorm.co/reference/get_events)
- [List people](https://developers.livestorm.co/reference/get_people)
- [List people attributes](https://developers.livestorm.co/reference/get_people-attributes)
- [Get a job](https://developers.livestorm.co/reference/get_jobs-id)
- [List chat messages from a session](https://developers.livestorm.co/reference/get_sessions-id-chat-messages)

Bulk session registrants (`POST …/sessions/{id}/people/bulk`) are not exposed on Write; a future Bulk-style integration would live outside this deep connector’s write path (see Salesforce bulk-write in-repo).
24 changes: 7 additions & 17 deletions providers/livestorm/metadata/schemas.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,31 +113,21 @@
}
}
},
"jobs": {
"displayName": "Jobs",
"path": "/jobs",
"users": {
"displayName": "Users",
"path": "/v1/users",
"responseKey": "data",
"docs": "https://developers.livestorm.co/reference/get_jobs-id",
"docs": "https://developers.livestorm.co/reference/post_users",
"fields": {
"id": {
"displayName": "Job Id",
"displayName": "User Id",
"valueType": "string",
"providerType": "string"
},
"status": {
"displayName": "Status",
"email": {
"displayName": "Email",
"valueType": "string",
"providerType": "string"
},
"created_at": {
"displayName": "Created At",
"valueType": "datetime",
"providerType": "datetime"
},
"updated_at": {
"displayName": "Updated At",
"valueType": "datetime",
"providerType": "datetime"
}
}
},
Expand Down
12 changes: 6 additions & 6 deletions providers/livestorm/metadata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func TestListObjectMetadata(t *testing.T) {
tests := []testroutines.Metadata{
{
Name: "Successful metadata for events and people",
Input: []string{"events", "people", "people_attributes", "jobs", "session_chat_messages"},
Input: []string{"events", "people", "people_attributes", "users", "session_chat_messages"},
Server: mockserver.Dummy(),
Comparator: testroutines.ComparatorSubsetMetadata,
Expected: &common.ListObjectMetadataResult{
Expand Down Expand Up @@ -66,16 +66,16 @@ func TestListObjectMetadata(t *testing.T) {
},
},
},
"jobs": {
DisplayName: "Jobs",
"users": {
DisplayName: "Users",
Fields: map[string]common.FieldMetadata{
"id": {
DisplayName: "Job Id",
DisplayName: "User Id",
ValueType: "string",
ProviderType: "string",
},
"status": {
DisplayName: "Status",
"email": {
DisplayName: "Email",
ValueType: "string",
ProviderType: "string",
},
Expand Down
13 changes: 0 additions & 13 deletions providers/livestorm/read.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ const (
apiVersion = "v1"
objectEvents = "events"
objectSessionChatMessages = "session_chat_messages"
objectJobs = "jobs"
)

// nolint:gochecknoglobals
Expand All @@ -31,7 +30,6 @@ var readSupportedObjects = datautils.NewStringSet(
"people",
"people_attributes",
"session_chat_messages",
"jobs",
)

func (c *Connector) buildReadRequest(ctx context.Context, params common.ReadParams) (*http.Request, error) {
Expand Down Expand Up @@ -68,8 +66,6 @@ func (c *Connector) buildReadURL(params common.ReadParams) (*urlbuilder.URL, err
return nil, common.ErrMissingObjects
case objectSessionChatMessages:
return c.buildSessionChatMessagesReadURL(params)
case objectJobs:
return c.buildJobReadURL(params)
default:
return c.buildGenericReadURL(params)
}
Expand All @@ -92,15 +88,6 @@ func (c *Connector) buildSessionChatMessagesReadURL(params common.ReadParams) (*
return endpointURL, nil
}

func (c *Connector) buildJobReadURL(params common.ReadParams) (*urlbuilder.URL, error) {
jobID := strings.TrimSpace(params.Filter)
if jobID == "" {
return nil, ErrJobIDRequired
}

return urlbuilder.New(c.ProviderInfo().BaseURL, apiVersion, "jobs", jobID)
}

func (c *Connector) buildGenericReadURL(params common.ReadParams) (*urlbuilder.URL, error) {
path, err := metadata.Schemas.FindURLPath(c.ProviderContext.Module(), params.ObjectName)
if err != nil {
Expand Down
42 changes: 0 additions & 42 deletions providers/livestorm/read_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ func TestRead(t *testing.T) { //nolint:funlen,gocognit,cyclop,maintidx
eventsMultipageBody := testutils.DataFromFile(t, "read-events-multipage.json")
peopleFirstPageBody := testutils.DataFromFile(t, "read-people-first-page.json")
chatMessagesBody := testutils.DataFromFile(t, "read-session-chat-messages.json")
jobBody := testutils.DataFromFile(t, "read-job.json")
peopleAttributesBody := testutils.DataFromFile(t, "read-people-attributes.json")

tests := []testroutines.Read{
Expand Down Expand Up @@ -55,12 +54,6 @@ func TestRead(t *testing.T) { //nolint:funlen,gocognit,cyclop,maintidx
Server: mockserver.Dummy(),
ExpectedErrs: []error{ErrSessionIDRequired},
},
{
Name: "Jobs require job id in filter",
Input: common.ReadParams{ObjectName: objectJobs, Fields: connectors.Fields("id")},
Server: mockserver.Dummy(),
ExpectedErrs: []error{ErrJobIDRequired},
},
{
Name: "Read events applies v1 path and incremental time filters",
Input: common.ReadParams{
Expand Down Expand Up @@ -260,41 +253,6 @@ func TestRead(t *testing.T) { //nolint:funlen,gocognit,cyclop,maintidx
},
},
},
{
Name: "Read job by id",
Input: common.ReadParams{
ObjectName: objectJobs,
Filter: "job_1",
Fields: connectors.Fields("status"),
},
Server: mockserver.Conditional{
Setup: mockserver.ContentJSON(),
If: mockcond.And{
mockcond.MethodGET(),
mockcond.Path("/v1/jobs/job_1"),
},
Then: mockserver.Response(http.StatusOK, jobBody),
}.Server(),
Comparator: testroutines.ComparatorSubsetRead,
Expected: &common.ReadResult{
Rows: 1,
Done: true,
Data: []common.ReadResultRow{
{
Fields: map[string]any{
"status": "done",
},
Raw: map[string]any{
"id": "job_1",
"type": "jobs",
"attributes": map[string]any{
"status": "done",
},
},
},
},
},
},
{
Name: "Read people attributes via generic object metadata path",
Input: common.ReadParams{
Expand Down
9 changes: 0 additions & 9 deletions providers/livestorm/test/read-job.json

This file was deleted.

Loading
Loading