diff --git a/model/actor_v2.go b/model/actor_v2.go new file mode 100644 index 0000000..4e8d2f8 --- /dev/null +++ b/model/actor_v2.go @@ -0,0 +1,34 @@ +package model + +import "github.com/incident-io/singer-tap/client" + +type actorV2 struct{} + +var ActorV2 actorV2 + +func (actorV2) Schema() Property { + return Property{ + Types: []string{"object"}, + Properties: map[string]Property{ + "api_key": Optional(APIKey.Schema()), + "user": Optional(UserV1.Schema()), + }, + } +} + +func (actorV2) Serialize(input client.ActorV2) map[string]any { + var user map[string]any + if input.User != nil { + user = UserV1.Serialize(*input.User) + } + + var apiKey map[string]any + if input.ApiKey != nil { + apiKey = APIKey.Serialize(*input.ApiKey) + } + + return map[string]any{ + "api_key": apiKey, + "user": user, + } +} diff --git a/model/api_key_v2.go b/model/api_key_v2.go new file mode 100644 index 0000000..991c367 --- /dev/null +++ b/model/api_key_v2.go @@ -0,0 +1,25 @@ +package model + +import "github.com/incident-io/singer-tap/client" + +type apiKeyV2 struct{} + +var APIKey apiKeyV2 + +func (apiKeyV2) Schema() Property { + return Property{ + Types: []string{"object"}, + Properties: map[string]Property{ + "id": { + Types: []string{"string"}, + }, + "name": { + Types: []string{"string"}, + }, + }, + } +} + +func (apiKeyV2) Serialize(input client.APIKeyV2) map[string]any { + return DumpToMap(input) +} diff --git a/model/base_types.go b/model/base_types.go new file mode 100644 index 0000000..8dc04f5 --- /dev/null +++ b/model/base_types.go @@ -0,0 +1,16 @@ +package model + +type dateTime struct{} + +var DateTime dateTime + +func (dateTime) Schema() Property { + return Property{ + Types: []string{"string"}, + CustomFormat: "date-time", + } +} + +func (dateTime) Serialize(input string) any { + return input +} diff --git a/model/custom_field_entry_v1.go b/model/custom_field_entry_v1.go new file mode 100644 index 0000000..ed40d8a --- /dev/null +++ b/model/custom_field_entry_v1.go @@ -0,0 +1,29 @@ +package model + +import ( + "github.com/incident-io/singer-tap/client" + "github.com/samber/lo" +) + +type customFieldEntryV1 struct{} + +var CustomFieldEntryV1 customFieldEntryV1 + +func (customFieldEntryV1) Schema() Property { + return Property{ + Types: []string{"object"}, + Properties: map[string]Property{ + "custom_field": CustomFieldTypeInfoV1.Schema(), + "values": ArrayOf(CustomFieldValueV1.Schema()), + }, + } +} + +func (customFieldEntryV1) Serialize(input client.CustomFieldEntryV1) map[string]any { + return map[string]any{ + "custom_field": CustomFieldTypeInfoV1.Serialize(input.CustomField), + "values": lo.Map(input.Values, func(value client.CustomFieldValueV1, _ int) map[string]any { + return CustomFieldValueV1.Serialize(value) + }), + } +} diff --git a/model/custom_field_option_v1.go b/model/custom_field_option_v1.go new file mode 100644 index 0000000..39afd04 --- /dev/null +++ b/model/custom_field_option_v1.go @@ -0,0 +1,35 @@ +package model + +import "github.com/incident-io/singer-tap/client" + +type customFieldOptionV1 struct{} + +var CustomFieldOptionV1 customFieldOptionV1 + +func (customFieldOptionV1) Schema() Property { + return Property{ + Types: []string{"object"}, + Properties: map[string]Property{ + "id": { + Types: []string{"string"}, + }, + "custom_field_id": { + Types: []string{"string"}, + }, + "sort_key": { + Types: []string{"integer"}, + }, + "value": { + Types: []string{"string"}, + }, + }, + } +} + +func (customFieldOptionV1) Serialize(input *client.CustomFieldOptionV1) map[string]any { + if input == nil { + return nil + } + + return DumpToMap(input) +} diff --git a/model/custom_field_type_info_v1.go b/model/custom_field_type_info_v1.go new file mode 100644 index 0000000..52e6e28 --- /dev/null +++ b/model/custom_field_type_info_v1.go @@ -0,0 +1,43 @@ +package model + +import ( + "github.com/incident-io/singer-tap/client" + "github.com/samber/lo" +) + +type customFieldTypeInfoV1 struct{} + +var CustomFieldTypeInfoV1 customFieldTypeInfoV1 + +func (customFieldTypeInfoV1) Schema() Property { + return Property{ + Types: []string{"object"}, + Properties: map[string]Property{ + "id": { + Types: []string{"string"}, + }, + "name": { + Types: []string{"string"}, + }, + "description": { + Types: []string{"string"}, + }, + "field_type": { + Types: []string{"string"}, + }, + "options": ArrayOf(CustomFieldOptionV1.Schema()), + }, + } +} + +func (customFieldTypeInfoV1) Serialize(input client.CustomFieldTypeInfoV1) map[string]any { + return map[string]any{ + "id": input.Id, + "name": input.Name, + "description": input.Description, + "field_type": input.FieldType, + "options": lo.Map(input.Options, func(option client.CustomFieldOptionV1, _ int) map[string]any { + return CustomFieldOptionV1.Serialize(&option) + }), + } +} diff --git a/model/custom_field_value_v1.go b/model/custom_field_value_v1.go new file mode 100644 index 0000000..a4b6d51 --- /dev/null +++ b/model/custom_field_value_v1.go @@ -0,0 +1,36 @@ +package model + +import "github.com/incident-io/singer-tap/client" + +type customFieldValueV1 struct{} + +var CustomFieldValueV1 customFieldValueV1 + +func (customFieldValueV1) Schema() Property { + return Property{ + Types: []string{"object"}, + Properties: map[string]Property{ + "value_link": { + Types: []string{"null", "string"}, + }, + "value_numeric": { + Types: []string{"null", "number"}, + }, + "value_text": { + Types: []string{"null", "string"}, + }, + "value_catalog_entry": Optional(EmbeddedCatalogEntryV1.Schema()), + "value_option": Optional(CustomFieldOptionV1.Schema()), + }, + } +} + +func (customFieldValueV1) Serialize(input client.CustomFieldValueV1) map[string]any { + return map[string]any{ + "value_link": input.ValueLink, + "value_numeric": input.ValueNumeric, + "value_text": input.ValueText, + "value_catalog_entry": EmbeddedCatalogEntryV1.Serialize(input.ValueCatalogEntry), + "value_option": CustomFieldOptionV1.Serialize(input.ValueOption), + } +} diff --git a/model/embedded_catalog_entry_v1.go b/model/embedded_catalog_entry_v1.go new file mode 100644 index 0000000..ad53bf9 --- /dev/null +++ b/model/embedded_catalog_entry_v1.go @@ -0,0 +1,40 @@ +package model + +import "github.com/incident-io/singer-tap/client" + +type embeddedCatalogEntryV1 struct{} + +var EmbeddedCatalogEntryV1 embeddedCatalogEntryV1 + +func (embeddedCatalogEntryV1) Schema() Property { + return Property{ + Types: []string{"object"}, + Properties: map[string]Property{ + "id": { + Types: []string{"string"}, + }, + "name": { + Types: []string{"string"}, + }, + "aliases": { + Types: []string{"array", "null"}, + }, + "external_id": { + Types: []string{"string", "null"}, + }, + }, + } +} + +func (embeddedCatalogEntryV1) Serialize(input *client.EmbeddedCatalogEntryV1) map[string]any { + if input == nil { + return nil + } + + return map[string]any{ + "id": input.Id, + "name": input.Name, + "aliases": input.Aliases, + "external_id": input.ExternalId, + } +} diff --git a/model/external_issue_reference_v2.go b/model/external_issue_reference_v2.go new file mode 100644 index 0000000..6926c70 --- /dev/null +++ b/model/external_issue_reference_v2.go @@ -0,0 +1,36 @@ +package model + +import "github.com/incident-io/singer-tap/client" + +type externalIssueReferenceV2 struct{} + +var ExternalIssueReferenceV2 externalIssueReferenceV2 + +func (externalIssueReferenceV2) Schema() Property { + return Property{ + Types: []string{"object", "null"}, + Properties: map[string]Property{ + "issue_name": { + Types: []string{"string"}, + }, + "issue_permalink": { + Types: []string{"string"}, + }, + "provider": { + Types: []string{"string"}, + }, + }, + } +} + +func (externalIssueReferenceV2) Serialize(input *client.ExternalIssueReferenceV2) map[string]any { + if input == nil { + return nil + } + + return map[string]any{ + "issue_name": input.IssueName, + "issue_permalink": input.IssuePermalink, + "provider": input.Provider, + } +} diff --git a/model/incident_role_assignment_v1.go b/model/incident_role_assignment_v1.go new file mode 100644 index 0000000..81d6979 --- /dev/null +++ b/model/incident_role_assignment_v1.go @@ -0,0 +1,29 @@ +package model + +import "github.com/incident-io/singer-tap/client" + +type incidentRoleAssignmentV1 struct{} + +var IncidentRoleAssignmentV1 incidentRoleAssignmentV1 + +func (incidentRoleAssignmentV1) Schema() Property { + return Property{ + Types: []string{"object"}, + Properties: map[string]Property{ + "assignee": Optional(UserV1.Schema()), + "role": IncidentRoleV1.Schema(), + }, + } +} + +func (incidentRoleAssignmentV1) Serialize(input client.IncidentRoleAssignmentV1) map[string]any { + var assignee map[string]any + if input.Assignee != nil { + assignee = UserV1.Serialize(*input.Assignee) + } + + return map[string]any{ + "assignee": assignee, + "role": IncidentRoleV1.Serialize(input.Role), + } +} diff --git a/model/incident_role_v1.go b/model/incident_role_v1.go new file mode 100644 index 0000000..4343b79 --- /dev/null +++ b/model/incident_role_v1.go @@ -0,0 +1,43 @@ +package model + +import "github.com/incident-io/singer-tap/client" + +type incidentRoleV1 struct{} + +var IncidentRoleV1 incidentRoleV1 + +func (incidentRoleV1) Schema() Property { + return Property{ + Types: []string{"object"}, + Properties: map[string]Property{ + "id": { + Types: []string{"string"}, + }, + "name": { + Types: []string{"string"}, + }, + "description": { + Types: []string{"string"}, + }, + "instructions": { + Types: []string{"string"}, + }, + "required": { + Types: []string{"boolean"}, + }, + "role_type": { + Types: []string{"string"}, + }, + "short_form": { + Types: []string{"string"}, + }, + "created_at": DateTime.Schema(), + "updated_at": DateTime.Schema(), + }, + } +} + +func (incidentRoleV1) Serialize(input client.IncidentRoleV1) map[string]any { + // Just flat convert everything into a map[string]any + return DumpToMap(input) +} diff --git a/model/incident_status_v1.go b/model/incident_status_v1.go new file mode 100644 index 0000000..b939dc2 --- /dev/null +++ b/model/incident_status_v1.go @@ -0,0 +1,37 @@ +package model + +import "github.com/incident-io/singer-tap/client" + +type incidentStatusV1 struct{} + +var IncidentStatusV1 incidentStatusV1 + +func (incidentStatusV1) Schema() Property { + return Property{ + Types: []string{"object"}, + Properties: map[string]Property{ + "id": { + Types: []string{"string"}, + }, + "name": { + Types: []string{"string"}, + }, + "category": { + Types: []string{"string"}, + }, + "description": { + Types: []string{"string"}, + }, + "rank": { + Types: []string{"integer"}, + }, + "created_at": DateTime.Schema(), + "updated_at": DateTime.Schema(), + }, + } +} + +func (incidentStatusV1) Serialize(input client.IncidentStatusV1) map[string]any { + // Just flat convert everything into a map[string]any + return DumpToMap(input) +} diff --git a/model/incident_timestamp_v2.go b/model/incident_timestamp_v2.go new file mode 100644 index 0000000..09ec273 --- /dev/null +++ b/model/incident_timestamp_v2.go @@ -0,0 +1,29 @@ +package model + +import "github.com/incident-io/singer-tap/client" + +type incidentTimestampV2 struct{} + +var IncidentTimestampV2 incidentTimestampV2 + +func (incidentTimestampV2) Schema() Property { + return Property{ + Types: []string{"object"}, + Properties: map[string]Property{ + "id": { + Types: []string{"string"}, + }, + "name": { + Types: []string{"string"}, + }, + "rank": { + Types: []string{"integer"}, + }, + }, + } +} + +func (incidentTimestampV2) Serialize(input client.IncidentTimestampV2) map[string]any { + // Just flat convert everything into a map[string]any + return DumpToMap(input) +} diff --git a/model/incident_timestamp_value_v2.go b/model/incident_timestamp_value_v2.go new file mode 100644 index 0000000..1831f46 --- /dev/null +++ b/model/incident_timestamp_value_v2.go @@ -0,0 +1,26 @@ +package model + +import "github.com/incident-io/singer-tap/client" + +type incidentTimestampValueV2 struct{} + +var IncidentTimestampValueV2 incidentTimestampValueV2 + +func (incidentTimestampValueV2) Schema() Property { + return Property{ + Types: []string{"object"}, + Properties: map[string]Property{ + "value": DateTime.Schema(), + }, + } +} + +func (incidentTimestampValueV2) Serialize(input *client.IncidentTimestampValueV2) map[string]any { + if input == nil { + return nil + } + + return map[string]any{ + "value": input.Value, + } +} diff --git a/model/incident_timestamp_with_value_v2.go b/model/incident_timestamp_with_value_v2.go new file mode 100644 index 0000000..d29d30b --- /dev/null +++ b/model/incident_timestamp_with_value_v2.go @@ -0,0 +1,24 @@ +package model + +import "github.com/incident-io/singer-tap/client" + +type incidentTimestampWithValueV2 struct{} + +var IncidentTimestampWithValueV2 incidentTimestampWithValueV2 + +func (incidentTimestampWithValueV2) Schema() Property { + return Property{ + Types: []string{"object"}, + Properties: map[string]Property{ + "incident_timestamp": IncidentTimestampV2.Schema(), + "value": Optional(IncidentTimestampValueV2.Schema()), + }, + } +} + +func (incidentTimestampWithValueV2) Serialize(input client.IncidentTimestampWithValueV2) map[string]any { + return map[string]any{ + "incident_timestamp": IncidentTimestampV2.Serialize(input.IncidentTimestamp), + "value": IncidentTimestampValueV2.Serialize(input.Value), + } +} diff --git a/model/incident_type_v1.go b/model/incident_type_v1.go new file mode 100644 index 0000000..f0a05d5 --- /dev/null +++ b/model/incident_type_v1.go @@ -0,0 +1,44 @@ +package model + +import "github.com/incident-io/singer-tap/client" + +type incidentTypeV1 struct{} + +var IncidentTypeV1 incidentTypeV1 + +func (incidentTypeV1) Schema() Property { + return Property{ + Types: []string{"object"}, + Properties: map[string]Property{ + "id": { + Types: []string{"string"}, + }, + "name": { + Types: []string{"string"}, + }, + "is_default": { + Types: []string{"boolean"}, + }, + "description": { + Types: []string{"string"}, + }, + "private_incidents_only": { + Types: []string{"boolean"}, + }, + "create_in_triage": { + Types: []string{"string"}, + }, + "created_at": DateTime.Schema(), + "updated_at": DateTime.Schema(), + }, + } +} + +func (incidentTypeV1) Serialize(input *client.IncidentTypeV1) map[string]any { + if input == nil { + return nil + } + + // Just flat convert everything into a map[string]any + return DumpToMap(input) +} diff --git a/model/incident_v2.go b/model/incident_v2.go new file mode 100644 index 0000000..1cbefe5 --- /dev/null +++ b/model/incident_v2.go @@ -0,0 +1,113 @@ +package model + +import ( + "github.com/incident-io/singer-tap/client" + "github.com/samber/lo" +) + +type incidentV2 struct{} + +var IncidentV2 incidentV2 + +func (incidentV2) Schema() Property { + return Property{ + Types: []string{"object"}, + Properties: map[string]Property{ + "id": { + Types: []string{"string"}, + }, + "name": { + Types: []string{"string"}, + }, + "call_url": { + Types: []string{"string", "null"}, + }, + "creator": ActorV2.Schema(), + "custom_field_entries": ArrayOf(CustomFieldEntryV1.Schema()), + "external_issue_reference": Optional(ExternalIssueReferenceV2.Schema()), + "incident_role_assignments": ArrayOf(IncidentRoleAssignmentV1.Schema()), + "incident_status": IncidentStatusV1.Schema(), + "incident_timestamp_values": Optional(ArrayOf(IncidentTimestampWithValueV2.Schema())), + "incident_type": Optional(IncidentTypeV1.Schema()), + "mode": { + Types: []string{"string"}, + }, + "permalink": { + Types: []string{"string", "null"}, + }, + "postmortem_document_url": { + Types: []string{"string", "null"}, + }, + "reference": { + Types: []string{"string"}, + }, + "severity": Optional(SeverityV2.Schema()), + "slack_channel_id": { + Types: []string{"string"}, + }, + "slack_channel_name": { + Types: []string{"string", "null"}, + }, + "slack_team_id": { + Types: []string{"string"}, + }, + "summary": { + Types: []string{"string", "null"}, + }, + "visibility": { + Types: []string{"string"}, + }, + "workload_minutes_late": { + Types: []string{"number", "null"}, + }, + "workload_minutes_sleeping": { + Types: []string{"number", "null"}, + }, + "workload_minutes_total": { + Types: []string{"number", "null"}, + }, + "workload_minutes_working": { + Types: []string{"number", "null"}, + }, + "created_at": DateTime.Schema(), + "updated_at": DateTime.Schema(), + }, + } +} + +func (incidentV2) Serialize(input client.IncidentV2) map[string]any { + return map[string]any{ + "id": input.Id, + "name": input.Name, + "call_url": input.CallUrl, + "creator": ActorV2.Serialize(input.Creator), + "custom_field_entries": lo.Map(input.CustomFieldEntries, func(entry client.CustomFieldEntryV1, _ int) map[string]any { + return CustomFieldEntryV1.Serialize(entry) + }), + "external_issue_reference": ExternalIssueReferenceV2.Serialize(input.ExternalIssueReference), + "incident_role_assignments": lo.Map(input.IncidentRoleAssignments, func(assignment client.IncidentRoleAssignmentV1, _ int) map[string]any { + return IncidentRoleAssignmentV1.Serialize(assignment) + }), + "incident_status": IncidentStatusV1.Serialize(input.IncidentStatus), + "incident_timestamp_values": lo.Map(*input.IncidentTimestampValues, func(timestamp client.IncidentTimestampWithValueV2, _ int) map[string]any { + return IncidentTimestampWithValueV2.Serialize(timestamp) + }), + "incident_type": IncidentTypeV1.Serialize(input.IncidentType), + "mode": input.Mode, + "permalink": input.Permalink, + "postmortem_document_url": input.PostmortemDocumentUrl, + "reference": input.Reference, + "severity": SeverityV2.Serialize(input.Severity), + "slack_channel_id": input.SlackChannelId, + "slack_channel_name": input.SlackChannelName, + "slack_team_id": input.SlackTeamId, + "summary": input.Summary, + "visibility": input.Visibility, + "workload_minutes_late": input.WorkloadMinutesLate, + "workload_minutes_sleeping": input.WorkloadMinutesSleeping, + "workload_minutes_total": input.WorkloadMinutesTotal, + "workload_minutes_working": input.WorkloadMinutesWorking, + "created_at": input.CreatedAt, + "updated_at": input.UpdatedAt, + } +} diff --git a/model/schema.go b/model/schema.go index f3b8614..1d23337 100644 --- a/model/schema.go +++ b/model/schema.go @@ -74,12 +74,12 @@ func DumpToMap(input interface{}) map[string]any { return structs.Map(input) } -func AsOptional(p Property) Property { +func Optional(p Property) Property { p.Types = append(p.Types, "null") return p } -func AsArray(p Property) Property { +func ArrayOf(p Property) Property { return Property{ Types: []string{"array"}, Items: &ArrayItem{ diff --git a/model/severity_v2.go b/model/severity_v2.go new file mode 100644 index 0000000..a4c1a9b --- /dev/null +++ b/model/severity_v2.go @@ -0,0 +1,37 @@ +package model + +import "github.com/incident-io/singer-tap/client" + +type severityV2 struct{} + +var SeverityV2 severityV2 + +func (severityV2) Schema() Property { + return Property{ + Types: []string{"object"}, + Properties: map[string]Property{ + "id": { + Types: []string{"string"}, + }, + "name": { + Types: []string{"string"}, + }, + "description": { + Types: []string{"string"}, + }, + "rank": { + Types: []string{"integer"}, + }, + "created_at": DateTime.Schema(), + "updated_at": DateTime.Schema(), + }, + } +} + +func (severityV2) Serialize(input *client.SeverityV2) map[string]any { + if input == nil { + return nil + } + + return DumpToMap(input) +} diff --git a/model/user_v1.go b/model/user_v1.go new file mode 100644 index 0000000..a39dae4 --- /dev/null +++ b/model/user_v1.go @@ -0,0 +1,37 @@ +package model + +import "github.com/incident-io/singer-tap/client" + +type userV1 struct{} + +var UserV1 userV1 + +func (userV1) Schema() Property { + return Property{ + Types: []string{"object"}, + Properties: map[string]Property{ + "id": { + Types: []string{"string"}, + }, + "name": { + Types: []string{"string"}, + }, + "email": { + Types: []string{"null", "string"}, + }, + "slack_user_id": { + Types: []string{"null", "string"}, + }, + }, + } +} + +func (userV1) Serialize(input client.UserV1) map[string]any { + // Deprecated role field needs removing - so build manually and omit it + return map[string]any{ + "id": input.Id, + "name": input.Name, + "email": input.Email, + "slack_user_id": input.SlackUserId, + } +} diff --git a/tap/stream_incidents.go b/tap/stream_incidents.go index 8449484..5adf133 100644 --- a/tap/stream_incidents.go +++ b/tap/stream_incidents.go @@ -22,15 +22,9 @@ func (s *StreamIncidents) Output() *Output { Type: OutputTypeSchema, Stream: "incidents", Schema: &model.Schema{ - Type: []string{"object"}, - Properties: map[string]model.Property{ - "id": { - Types: []string{"string"}, - }, - "name": { - Types: []string{"string"}, - }, - }, + HasAdditionalProperties: false, + Type: []string{"object"}, + Properties: model.IncidentV2.Schema().Properties, }, KeyProperties: []string{"id"}, BookmarkProperties: []string{}, @@ -55,10 +49,7 @@ func (s *StreamIncidents) GetRecords(ctx context.Context, logger kitlog.Logger, } for _, element := range page.JSON200.Incidents { - results = append(results, map[string]any{ - "id": element.Id, - "name": element.Name, - }) + results = append(results, model.IncidentV2.Serialize(element)) } if count := len(page.JSON200.Incidents); count == 0 { return results, nil // end pagination diff --git a/tap/stream_users.go b/tap/stream_users.go new file mode 100644 index 0000000..43ee3e3 --- /dev/null +++ b/tap/stream_users.go @@ -0,0 +1,60 @@ +package tap + +import ( + "context" + + kitlog "github.com/go-kit/log" + "github.com/incident-io/singer-tap/client" + "github.com/incident-io/singer-tap/model" + "github.com/pkg/errors" + "github.com/samber/lo" +) + +func init() { + register(&StreamUsers{}) +} + +type StreamUsers struct { +} + +func (s *StreamUsers) Output() *Output { + return &Output{ + Type: OutputTypeSchema, + Stream: "users", + Schema: &model.Schema{ + HasAdditionalProperties: false, + Type: []string{"object"}, + Properties: model.UserV1.Schema().Properties, + }, + KeyProperties: []string{"id"}, + BookmarkProperties: []string{}, + } +} + +func (s *StreamUsers) GetRecords(ctx context.Context, logger kitlog.Logger, cl *client.ClientWithResponses) ([]map[string]any, error) { + var ( + after *string + pageSize = 250 + results = []map[string]any{} + ) + + for { + logger.Log("msg", "loading page", "page_size", pageSize, "after", after) + page, err := cl.UsersV2ListWithResponse(ctx, &client.UsersV2ListParams{ + PageSize: &pageSize, + After: after, + }) + if err != nil { + return nil, errors.Wrap(err, "listing incidents") + } + + for _, element := range page.JSON200.Users { + results = append(results, model.UserV1.Serialize(element)) + } + if count := len(page.JSON200.Users); count == 0 { + return results, nil // end pagination + } else { + after = lo.ToPtr(page.JSON200.Users[count-1].Id) + } + } +}