diff --git a/lib/workos.rb b/lib/workos.rb index d695f1b2..17dcffd2 100644 --- a/lib/workos.rb +++ b/lib/workos.rb @@ -31,6 +31,7 @@ def self.key! autoload :AuditTrail, 'workos/audit_trail' autoload :Connection, 'workos/connection' autoload :DirectorySync, 'workos/directory_sync' + autoload :Organization, 'workos/organization' autoload :Portal, 'workos/portal' autoload :Profile, 'workos/profile' autoload :SSO, 'workos/sso' diff --git a/lib/workos/organization.rb b/lib/workos/organization.rb new file mode 100644 index 00000000..19cb0006 --- /dev/null +++ b/lib/workos/organization.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true +# typed: true + +require 'json' + +module WorkOS + # The Organization class provides a lightweight wrapper around + # a WorkOS Organization resource. This class is not meant to be instantiated + # in user space, and is instantiated internally but exposed. + class Organization + extend T::Sig + + attr_accessor :id, :domains, :name + + sig { params(json: String).void } + def initialize(json) + raw = parse_json(json) + + @id = T.let(raw.id, String) + @name = T.let(raw.name, String) + @domains = T.let(raw.domains, Array) + end + + def to_json(*) + { + id: id, + name: name, + domains: domains, + } + end + + private + + sig do + params( + json_string: String, + ).returns(WorkOS::Types::OrganizationStruct) + end + def parse_json(json_string) + hash = JSON.parse(json_string, symbolize_names: true) + + WorkOS::Types::OrganizationStruct.new( + id: hash[:id], + name: hash[:name], + domains: hash[:domains], + ) + end + end +end diff --git a/lib/workos/portal.rb b/lib/workos/portal.rb index bd3b37d3..7224743a 100644 --- a/lib/workos/portal.rb +++ b/lib/workos/portal.rb @@ -12,6 +12,30 @@ class << self include Base include Client + # Create an organization + # + # @param [Array] domains List of domains that belong to the + # organization + # @param [String] name A unique, descriptive name for the organization + sig do + params( + domains: T::Array[String], + name: String, + ).returns(WorkOS::Organization) + end + def create_organization(domains:, name:) + request = post_request( + auth: true, + body: { domains: domains, name: name }, + path: '/organizations', + ) + + response = execute_request(request: request) + check_and_raise_organization_error(response: response) + + WorkOS::Organization.new(response.body) + end + # Retrieve a list of organizations that have connections configured # within your WorkOS dashboard. # @@ -46,6 +70,29 @@ def list_organizations(options = {}) ) end # rubocop:enable Metrics/MethodLength + + private + + sig { params(response: Net::HTTPResponse).void } + # rubocop:disable Metrics/MethodLength + def check_and_raise_organization_error(response:) + begin + body = JSON.parse(response.body) + return unless body['message'] + + message = body['message'] + request_id = response['x-request-id'] + rescue StandardError + message = 'Something went wrong' + end + + raise APIError.new( + message: message, + http_status: nil, + request_id: request_id, + ) + end + # rubocop:enable Metrics/MethodLength end end end diff --git a/lib/workos/types.rb b/lib/workos/types.rb index afc37ee6..7319a6a3 100644 --- a/lib/workos/types.rb +++ b/lib/workos/types.rb @@ -7,6 +7,7 @@ module WorkOS module Types require_relative 'types/connection_struct' require_relative 'types/list_struct' + require_relative 'types/organization_struct' require_relative 'types/profile_struct' require_relative 'types/provider_enum' end diff --git a/lib/workos/types/organization_struct.rb b/lib/workos/types/organization_struct.rb new file mode 100644 index 00000000..bd1c0d38 --- /dev/null +++ b/lib/workos/types/organization_struct.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +# typed: strict + +module WorkOS + module Types + # This OrganizationStruct acts as a typed interface + # for the Organization class + class OrganizationStruct < T::Struct + const :id, String + const :name, String + const :domains, T::Array[T.untyped] + end + end +end diff --git a/spec/lib/workos/portal_spec.rb b/spec/lib/workos/portal_spec.rb index b724ff8f..5a5dad99 100644 --- a/spec/lib/workos/portal_spec.rb +++ b/spec/lib/workos/portal_spec.rb @@ -10,6 +10,39 @@ WorkOS.key = nil end + describe '.create_organization' do + context 'with valid payload' do + it 'creates an organization' do + VCR.use_cassette 'organization/create' do + organization = described_class.create_organization( + domains: ['example.com'], + name: 'Test Organization', + ) + + expect(organization.id).to eq('org_01EHT88Z8J8795GZNQ4ZP1J81T') + expect(organization.name).to eq('Test Organization') + expect(organization.domains.first[:domain]).to eq('example.com') + end + end + end + + context 'with an invalid payload' do + it 'returns an error' do + VCR.use_cassette 'organization/create_invalid' do + expect do + described_class.create_organization( + domains: ['example.com'], + name: 'Test Organization 2', + ) + end.to raise_error( + WorkOS::APIError, + /An Organization with the domain example.com already exists/, + ) + end + end + end + end + describe '.list_organizations' do context 'with no options' do it 'returns organizations and metadata' do diff --git a/spec/support/fixtures/vcr_cassettes/organization/create.yml b/spec/support/fixtures/vcr_cassettes/organization/create.yml new file mode 100644 index 00000000..3912ec67 --- /dev/null +++ b/spec/support/fixtures/vcr_cassettes/organization/create.yml @@ -0,0 +1,72 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.workos.com/organizations + body: + encoding: UTF-8 + string: '{"domains":["example.com"],"name":"Test Organization"}' + headers: + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - WorkOS; ruby/2.7.1; x86_64-darwin19; v0.5.0 + Authorization: + - Bearer + response: + status: + code: 201 + message: Created + headers: + Server: + - Cowboy + Connection: + - keep-alive + Vary: + - Origin, Accept-Encoding + Access-Control-Allow-Credentials: + - 'true' + Content-Security-Policy: + - 'default-src ''self'';base-uri ''self'';block-all-mixed-content;font-src ''self'' + https: data:;frame-ancestors ''self'';img-src ''self'' data:;object-src ''none'';script-src + ''self'';script-src-attr ''none'';style-src ''self'' https: ''unsafe-inline'';upgrade-insecure-requests' + X-Dns-Prefetch-Control: + - 'off' + Expect-Ct: + - max-age=0 + X-Frame-Options: + - SAMEORIGIN + Strict-Transport-Security: + - max-age=15552000; includeSubDomains + X-Download-Options: + - noopen + X-Content-Type-Options: + - nosniff + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - no-referrer + X-Xss-Protection: + - '0' + X-Request-Id: + - bcddfa8b-3a12-4d48-b265-6094a26225a4 + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '203' + Etag: + - W/"cb-T4+GTGrJeuAmC0PAjcaSX5ZXL18" + Date: + - Wed, 09 Sep 2020 20:17:53 GMT + Via: + - 1.1 vegur + body: + encoding: UTF-8 + string: '{"name":"Test Organization","object":"organization","id":"org_01EHT88Z8J8795GZNQ4ZP1J81T","domains":[{"domain":"example.com","object":"organization_domain","id":"org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8"}]}' + http_version: + recorded_at: Wed, 09 Sep 2020 20:17:54 GMT +recorded_with: VCR 5.0.0 \ No newline at end of file diff --git a/spec/support/fixtures/vcr_cassettes/organization/create_invalid.yml b/spec/support/fixtures/vcr_cassettes/organization/create_invalid.yml new file mode 100644 index 00000000..610b6fe1 --- /dev/null +++ b/spec/support/fixtures/vcr_cassettes/organization/create_invalid.yml @@ -0,0 +1,72 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.workos.com/organizations + body: + encoding: UTF-8 + string: '{"domains":["example.com"],"name":"Test Organization 2"}' + headers: + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - WorkOS; ruby/2.7.1; x86_64-darwin19; v0.5.0 + Authorization: + - Bearer + response: + status: + code: 409 + message: Conflict + headers: + Server: + - Cowboy + Connection: + - keep-alive + Vary: + - Origin, Accept-Encoding + Access-Control-Allow-Credentials: + - 'true' + Content-Security-Policy: + - 'default-src ''self'';base-uri ''self'';block-all-mixed-content;font-src ''self'' + https: data:;frame-ancestors ''self'';img-src ''self'' data:;object-src ''none'';script-src + ''self'';script-src-attr ''none'';style-src ''self'' https: ''unsafe-inline'';upgrade-insecure-requests' + X-Dns-Prefetch-Control: + - 'off' + Expect-Ct: + - max-age=0 + X-Frame-Options: + - SAMEORIGIN + Strict-Transport-Security: + - max-age=15552000; includeSubDomains + X-Download-Options: + - noopen + X-Content-Type-Options: + - nosniff + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - no-referrer + X-Xss-Protection: + - '0' + X-Request-Id: + - 929940d6-33dd-404c-9856-eca6cc606d28 + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '73' + Etag: + - W/"49-8i1S2EtfSciiA8rvGWbYFNlSlhw" + Date: + - Wed, 09 Sep 2020 21:26:03 GMT + Via: + - 1.1 vegur + body: + encoding: UTF-8 + string: '{"message":"An Organization with the domain example.com already exists."}' + http_version: + recorded_at: Wed, 09 Sep 2020 21:26:03 GMT +recorded_with: VCR 5.0.0 \ No newline at end of file