Skip to content

Commit

Permalink
[CON-808] Iterable: Write
Browse files Browse the repository at this point in the history
  • Loading branch information
Cobalt0s committed Dec 20, 2024
1 parent 5a28fce commit 65ce017
Show file tree
Hide file tree
Showing 13 changed files with 751 additions and 0 deletions.
95 changes: 95 additions & 0 deletions providers/iterable/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Description

This is an exhaustive list of API endpoints which are excluded from Write/Delete connector implementation and therefore are available only via proxy.

# Excluded Endpoints

**Authentication**
- [/api/auth/jwts/invalidate](https://api.iterable.com/api/docs#users_Invalidate_JWT) - Invalidate all JWTs issued for a user

**Campaigns**
- [/api/campaigns/abort](https://api.iterable.com/api/docs#campaigns_abort_campaign) - Abort Campaign
- [/api/campaigns/activateTriggered](https://api.iterable.com/api/docs#campaigns_activate_triggered_campaign) - Activate a triggered campaign
- [/api/campaigns/archive](https://api.iterable.com/api/docs#campaigns_archive_campaigns) - Archive campaigns
- [/api/campaigns/cancel](https://api.iterable.com/api/docs#campaigns_cancel_campaign) - Cancel a scheduled or recurring campaign
- [/api/campaigns/deactivateTriggered](https://api.iterable.com/api/docs#campaigns_Deactivate_triggered_campaign) - Deactivate a triggered campaign
- [/api/campaigns/trigger](https://api.iterable.com/api/docs#campaigns_trigger_campaign) - Trigger a campaign

**Commerce**
- [/api/commerce/trackPurchase](https://api.iterable.com/api/docs#commerce_trackPurchase) - Track a purchase
- [/api/commerce/updateCart](https://api.iterable.com/api/docs#commerce_updateCart) - Update a user's shopping cart items

**Emails**
- [/api/email/cancel](https://api.iterable.com/api/docs#email_cancel) - Cancel an email to a user
- [/api/email/target](https://api.iterable.com/api/docs#email_target) - Send an email to an email address

**Message Events**
- [/api/embedded-messaging/events/click](https://api.iterable.com/api/docs#events_embedded_track_click) - Track an embedded message click
- [/api/embedded-messaging/events/received](https://api.iterable.com/api/docs#events_embedded_track_received) - Track an embedded message received event
- [/api/embedded-messaging/events/session](https://api.iterable.com/api/docs#events_embedded_track_impression) - Track an embedded message session and related impressions

**Events**
- [/api/events/inAppConsume](https://api.iterable.com/api/docs#events_inAppConsume) - Consume or delete an in-app message
- [/api/events/trackBulk](https://api.iterable.com/api/docs#events_trackBulk) - Bulk track events
- [/api/events/trackInAppClick](https://api.iterable.com/api/docs#events_trackInAppClick) - Track an in-app message click
- [/api/events/trackInAppClose](https://api.iterable.com/api/docs#events_trackInAppClose) - Track the closing of an in-app message
- [/api/events/trackInAppDelivery](https://api.iterable.com/api/docs#events_trackInAppDelivery) - Track the delivery of an in-app message
- [/api/events/trackInAppOpen](https://api.iterable.com/api/docs#events_trackInAppOpen) - Track an in-app message open
- [/api/events/trackPushOpen](https://api.iterable.com/api/docs#events_trackPushOpen) - Track a mobile push open
- [/api/events/trackWebPushClick](https://api.iterable.com/api/docs#events_trackWebPushClick) - Track a web push click
- [/api/events/track](https://api.iterable.com/api/docs#events_track) - Track an event
- [/api/export/start](https://api.iterable.com/api/docs#export_startExport) - Start export

**In-App Notifications**
- [/api/inApp/cancel](https://api.iterable.com/api/docs#In-app_cancel) - Cancel a scheduled in-app message
- [/api/inApp/target](https://api.iterable.com/api/docs#In-app_target) - Send an in-app notification to a user

**Lists**
- [/api/lists/subscribe](https://api.iterable.com/api/docs#lists_subscribe) - Add subscribers to list
- [/api/lists/unsubscribe](https://api.iterable.com/api/docs#lists_unsubscribe) - Remove users from a list
- [/api/lists](https://api.iterable.com/api/docs#lists_create) - Create a static list

**Push Notifications**
- [/api/push/cancel](https://api.iterable.com/api/docs#push_cancel) - Cancel a push notification to a user
- [/api/push/target](https://api.iterable.com/api/docs#push_target) - Send push notification to user

**SMS**
- [/api/sms/cancel](https://api.iterable.com/api/docs#SMS_cancel) - Cancel an SMS to a user
- [/api/sms/target](https://api.iterable.com/api/docs#SMS_target) - Send SMS notification to user

**Subscription**
- [/api/subscriptions/subscribeToDoubleOptIn](https://api.iterable.com/api/docs#subscriptions_subscribeSingleUserToDoubleOptIn) - Trigger a double opt-in subscription flow

**Templates**
- [/api/templates/bulkDelete](https://api.iterable.com/api/docs#templates_bulk_delete_templates) - Bulk delete templates

**Users**
- [/api/users/bulkUpdateSubscriptions](https://api.iterable.com/api/docs#users_bulkUpdateSubscriptions) - Bulk update user subscriptions
- [/api/users/bulkUpdate](https://api.iterable.com/api/docs#users_bulkUpdateUser) - Bulk update user data
- [/api/users/disableDevice](https://api.iterable.com/api/docs#users_disableDevice) - Disable pushes to a mobile device
- [/api/users/forget](https://api.iterable.com/api/docs#users_forget) - Forget a user in compliance with GDPR
- [/api/users/registerBrowserToken](https://api.iterable.com/api/docs#users_registerBrowserToken) - Register a browser token for web push
- [/api/users/registerDeviceToken](https://api.iterable.com/api/docs#users_registerDeviceToken) - Register a device token for push
- [/api/users/unforget](https://api.iterable.com/api/docs#users_unforget) - Unforget a user in compliance with GDPR
- [/api/users/updateEmail](https://api.iterable.com/api/docs#users_updateEmail) - Update user email
- [/api/users/updateSubscriptions](https://api.iterable.com/api/docs#users_updateSubscriptions) - Update user subscriptions

**Verifications**
- [/api/verify/sms/begin](https://api.iterable.com/api/docs#Verify_beginSmsVerification) - Begin SMS Verification
- [/api/verify/sms/check](https://api.iterable.com/api/docs#Verify_checkSmsVerification) - Check SMS Verification Code

**Web Push Notification**
- [/api/webPush/cancel](https://api.iterable.com/api/docs#webPush_cancel) - Cancel a web push notification to a user
- [/api/webPush/target](https://api.iterable.com/api/docs#webPush_target) - Send web push notification to user

**Workflow**
- [/api/workflows/triggerWorkflow](https://api.iterable.com/api/docs#workflows_triggerWorkflow) - Trigger a journey (workflow)


Endpoints that are performing alike operations are excluded as well.
In other words they are covered by related endpoint. Excluded similiar endpoints are as follows:
- [/api/templates/email/update](https://api.iterable.com/api/docs#templates_updateEmailTemplate) - Update email template
- [/api/templates/inapp/update](https://api.iterable.com/api/docs#templates_updateInAppTemplate) - Update in-app template
- [/api/templates/push/update](https://api.iterable.com/api/docs#templates_updatePushTemplate) - Update push template
- [/api/templates/sms/update](https://api.iterable.com/api/docs#templates_updateSMSTemplate) - Update SMS template
- [/api/users/{email}](https://api.iterable.com/api/docs#users_delete_0) - Delete a user by email
12 changes: 12 additions & 0 deletions providers/iterable/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ func (c *Connector) getReadURL(objectName string) (*urlbuilder.URL, error) {
return urlbuilder.New(c.BaseURL, path)
}

func (c *Connector) getWriteURL(objectName string) (*urlbuilder.URL, error) {
path := supportedObjectsByWrite[objectName]

return urlbuilder.New(c.BaseURL, path)
}

func (c *Connector) getDeleteURL(objectName, recordID string) (*urlbuilder.URL, error) {
path := supportedObjectsByDelete[objectName]

return urlbuilder.New(c.BaseURL, path, recordID)
}

func (c *Connector) setBaseURL(newURL string) {
c.BaseURL = newURL
c.Client.HTTPClient.Base = newURL
Expand Down
34 changes: 34 additions & 0 deletions providers/iterable/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package iterable

import (
"context"

"github.com/amp-labs/connectors/common"
)

func (c *Connector) Delete(ctx context.Context, config common.DeleteParams) (*common.DeleteResult, error) {
if err := config.ValidateParams(); err != nil {
return nil, err
}

if !supportedObjectsByDelete.Has(config.ObjectName) {
// Removing tags is the only to be supported at this time.
// https://developer.instantly.ai/tags/delete-a-tag
return nil, common.ErrOperationNotSupportedForObject
}

url, err := c.getDeleteURL(config.ObjectName, config.RecordId)
if err != nil {
return nil, err
}

// 200 OK is expected
_, err = c.Client.Delete(ctx, url.String())
if err != nil {
return nil, err
}

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

import (
"errors"
"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"
"github.com/amp-labs/connectors/test/utils/testutils"
)

func TestDelete(t *testing.T) { // nolint:funlen,cyclop
t.Parallel()

responseNotFoundErr := testutils.DataFromFile(t, "delete-tag-missing.json")
responseTag := testutils.DataFromFile(t, "delete-tag.json")

tests := []testroutines.Delete{
{
Name: "Write object must be included",
Server: mockserver.Dummy(),
ExpectedErrs: []error{common.ErrMissingObjects},
},
{
Name: "Write object and its ID must be included",
Input: common.DeleteParams{ObjectName: "tags"},
Server: mockserver.Dummy(),
ExpectedErrs: []error{common.ErrMissingRecordID},
},
{
Name: "Cannot remove unknown object",
Input: common.DeleteParams{ObjectName: "coupons", RecordId: "132"},
Server: mockserver.Dummy(),
ExpectedErrs: []error{
common.ErrOperationNotSupportedForObject,
},
},
{
Name: "Cannot remove missing tag",
Input: common.DeleteParams{ObjectName: "tags", RecordId: "5043"},
Server: mockserver.Fixed{
Setup: mockserver.ContentJSON(),
Always: mockserver.Response(http.StatusNotFound, responseNotFoundErr),
}.Server(),
ExpectedErrs: []error{
common.ErrBadRequest,
errors.New(`Not Found`), // nolint:goerr113
},
},
{
Name: "Successful delete",
Input: common.DeleteParams{ObjectName: "tags", RecordId: "5043"},
Server: mockserver.Conditional{
Setup: mockserver.ContentJSON(),
If: mockcond.And{
mockcond.MethodDELETE(),
mockcond.PathSuffix("custom-tag/5043"),
},
Then: mockserver.Response(http.StatusOK, responseTag),
}.Server(),
Expected: &common.DeleteResult{Success: true},
ExpectedErrs: nil,
},
}

for _, tt := range tests {
// nolint:varnamelen
t.Run(tt.Name, func(t *testing.T) {
t.Parallel()

tt.Run(t, func() (connectors.DeleteConnector, error) {
return constructTestConnector(tt.Server.URL)
})
})
}
}
39 changes: 39 additions & 0 deletions providers/iterable/objectNames.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,42 @@ var incrementalReadObjects = datautils.NewSet( //nolint:gochecknoglobals

// Supported object names can be found under schemas.json.
var supportedObjectsByRead = metadata.Schemas.ObjectNames() //nolint:gochecknoglobals

var supportedObjectsByWrite = datautils.Map[string, string]{ //nolint:gochecknoglobals
// https://api.iterable.com/api/docs#campaigns_create_campaign
"campaigns": "/api/campaigns/create",
// https://api.iterable.com/api/docs#catalogs_createCatalog
// This endpoint doesn't use payload! Catalog name comes from the path {catalogName}.
objectNameCatalogs: "/api/catalogs",
// https://api.iterable.com/api/docs#lists_create
"lists": "/api/lists",
// https://api.iterable.com/api/docs#users_updateUser
"users": "/api/users/update", // Update or create user.
// https://api.iterable.com/api/docs#webhooks_updateWebhook
"webhooks": "/api/webhooks", // Update webhook

//
// Template objects.
//
// https://api.iterable.com/api/docs#templates_upsertEmailTemplate
"templatesEmail": "/api/templates/email/upsert",
// https://api.iterable.com/api/docs#templates_upsertInAppTemplate
"templatesInApp": "/api/templates/inapp/upsert",
// https://api.iterable.com/api/docs#templates_upsertPushTemplate
"templatesPush": "/api/templates/push/upsert",
// https://api.iterable.com/api/docs#templates_upsertSMSTemplate
"templatesSMS": "/api/templates/sms/upsert",
}

var supportedObjectsByDelete = datautils.Map[string, string]{ //nolint:gochecknoglobals
// https://api.iterable.com/api/docs#catalogs_deleteCatalog
objectNameCatalogs: "/api/catalogs", // by catalogName
// https://api.iterable.com/api/docs#export_cancelExport
"exports": "/api/export", // by jobId
// https://api.iterable.com/api/docs#lists_delete
"lists": "/api/lists", // by listId
// https://api.iterable.com/api/docs#metadata_delete
"metadata": "/api/metadata", // by table
// https://api.iterable.com/api/docs#users_delete
"users": "/api/users/byUserId", // by userId
}
5 changes: 5 additions & 0 deletions providers/iterable/test/delete-list.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"msg": "List 5052803 in Project 24382 was successfully deleted.",
"code": "Success",
"params": null
}
5 changes: 5 additions & 0 deletions providers/iterable/test/delete-missing-list-bad-request.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"msg": "error.lists.deleteFailed(5052803)",
"code": "BadParams",
"params": null
}
9 changes: 9 additions & 0 deletions providers/iterable/test/write-catalog.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"msg": "",
"code": "Success",
"params": {
"id": 125254,
"name": "newPostmanCatalog",
"url": "/api/catalogs/newPostmanCatalog"
}
}
49 changes: 49 additions & 0 deletions providers/iterable/test/write-template-bad-request.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">

<head>
<title>Bad Request</title>

<style nonce="sr/x98ZfDIGnvHzHKmvWRA==" type="text/css">
html,
body,
pre {
margin: 0;
padding: 0;
font-family: Monaco, 'Lucida Console', monospace;
background: #ECECEC;
}

h1 {
margin: 0;
background: #AD632A;
padding: 20px 45px;
color: #fff;
text-shadow: 1px 1px 1px rgba(0, 0, 0, .3);
border-bottom: 1px solid #9F5805;
font-size: 28px;
}

p#detail {
margin: 0;
padding: 15px 45px;
background: #F6A960;
border-top: 4px solid #D29052;
color: #733512;
text-shadow: 1px 1px 1px rgba(255, 255, 255, .3);
font-size: 14px;
border-bottom: 1px solid #BA7F5B;
}
</style>
</head>

<body>
<h1>Bad Request</h1>

<p id="detail">
For request 'POST /api/templates/push/upsert' [Invalid Json: No content to map due to end-of-input]
</p>

</body>

</html>
7 changes: 7 additions & 0 deletions providers/iterable/test/write-template-invalid-request.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"msg": "[/api/templates/push/upsert] Invalid JSON body",
"code": "BadJsonBody",
"params": {
"obj.buttons[0].actionIcon.iconType": "String value expected"
}
}
Loading

0 comments on commit 65ce017

Please sign in to comment.