mirah-ruby 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +93 -0
  3. data/.gitignore +14 -0
  4. data/.overcommit.yml +48 -0
  5. data/.overcommit_gems.rb +9 -0
  6. data/.rspec +3 -0
  7. data/.rubocop.yml +54 -0
  8. data/.yardopts +1 -0
  9. data/CODE_OF_CONDUCT.md +74 -0
  10. data/Gemfile +6 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +187 -0
  13. data/Rakefile +24 -0
  14. data/bin/autocorrect +28 -0
  15. data/bin/console +14 -0
  16. data/bin/setup +8 -0
  17. data/bin/setup_overcommit +12 -0
  18. data/lib/mirah.rb +44 -0
  19. data/lib/mirah/base_input_object.rb +65 -0
  20. data/lib/mirah/base_object.rb +98 -0
  21. data/lib/mirah/client.rb +420 -0
  22. data/lib/mirah/collection.rb +98 -0
  23. data/lib/mirah/data/appointment.rb +65 -0
  24. data/lib/mirah/data/organization.rb +31 -0
  25. data/lib/mirah/data/page_info.rb +40 -0
  26. data/lib/mirah/data/patient.rb +46 -0
  27. data/lib/mirah/data/practitioner.rb +53 -0
  28. data/lib/mirah/errors.rb +39 -0
  29. data/lib/mirah/filters.rb +7 -0
  30. data/lib/mirah/filters/appointment_filters.rb +16 -0
  31. data/lib/mirah/filters/organization_filters.rb +16 -0
  32. data/lib/mirah/filters/paging.rb +33 -0
  33. data/lib/mirah/filters/patient_filters.rb +16 -0
  34. data/lib/mirah/filters/practitioner_filters.rb +16 -0
  35. data/lib/mirah/graphql.rb +33 -0
  36. data/lib/mirah/graphql/fragments.rb +97 -0
  37. data/lib/mirah/graphql/mutations.rb +72 -0
  38. data/lib/mirah/graphql/queries.rb +196 -0
  39. data/lib/mirah/inputs.rb +7 -0
  40. data/lib/mirah/inputs/appointment_input.rb +40 -0
  41. data/lib/mirah/inputs/organization_input.rb +20 -0
  42. data/lib/mirah/inputs/patient_input.rb +40 -0
  43. data/lib/mirah/inputs/practitioner_input.rb +44 -0
  44. data/lib/mirah/push_result.rb +40 -0
  45. data/lib/mirah/serializers.rb +57 -0
  46. data/lib/mirah/version.rb +5 -0
  47. data/mirah-ruby.gemspec +41 -0
  48. data/schema.json +8936 -0
  49. metadata +221 -0
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
9
+
10
+ require 'graphql/client'
11
+ require 'graphql/client/http'
12
+
13
+ task :dump_schema do
14
+ http = GraphQL::Client::HTTP.new('http://localhost:3000/integration_api/graphqlschema')
15
+ ::GraphQL::Client.dump_schema(http, 'schema.json')
16
+ end
17
+
18
+ # Run by typing: rake yard
19
+
20
+ PATH = File.dirname(__FILE__)
21
+
22
+ require 'yard'
23
+
24
+ YARD::Rake::YardocTask.new
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'yaml'
5
+
6
+ config = YAML.load_file('.rubocop.yml')
7
+
8
+ # Check if the file is excluded from all cops
9
+ exclude_file = lambda do |file|
10
+ config['AllCops']['Exclude'].any? do |pattern|
11
+ File.fnmatch(pattern, file, File::FNM_PATHNAME)
12
+ end
13
+ end
14
+
15
+ files = `git diff --name-only --diff-filter=ACMR --ignore-submodules=all --staged`
16
+ .split(/\n/)
17
+ .reject(&exclude_file)
18
+
19
+ if files.empty?
20
+ puts 'There is nothing staged to correct!'
21
+ exit
22
+ end
23
+
24
+ system({ 'BUNDLE_GEMFILE' => '.overcommit_gems.rb' },
25
+ "bundle exec rubocop --auto-correct --config=.rubocop.yml #{files.join(' ')}")
26
+
27
+ puts
28
+ puts 'Corrections applied. Review unstaged changes and apply any required manual corrections.'
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "mirah"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ require 'pathname'
3
+
4
+ # path to your application root.
5
+ APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
6
+
7
+ Dir.chdir APP_ROOT do
8
+ puts "== Installing overcommit =="
9
+ system({"BUNDLE_GEMFILE" => ".overcommit_gems.rb"}, "bundle check || bundle install")
10
+ system({"BUNDLE_GEMFILE" => ".overcommit_gems.rb"}, "bundle exec overcommit --install")
11
+ system({"BUNDLE_GEMFILE" => ".overcommit_gems.rb"}, "bundle exec overcommit --sign")
12
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ require 'graphql/client'
6
+ require 'graphql/client/http'
7
+
8
+ require 'active_support/hash_with_indifferent_access'
9
+ require 'active_support/inflector'
10
+
11
+ require 'mirah/version'
12
+ require 'mirah/errors'
13
+ require 'mirah/serializers'
14
+ require 'mirah/base_object'
15
+ require 'mirah/collection'
16
+ require 'mirah/push_result'
17
+ require 'mirah/graphql'
18
+
19
+ Dir[File.dirname(__FILE__) + '/mirah/data/**/*.rb'].sort.each do |file|
20
+ require file
21
+ end
22
+
23
+ require 'mirah/graphql/fragments'
24
+ require 'mirah/graphql/queries'
25
+ require 'mirah/graphql/mutations'
26
+
27
+ require 'mirah/filters'
28
+
29
+ Dir[File.dirname(__FILE__) + '/mirah/filters/**/*.rb'].sort.each do |file|
30
+ require file
31
+ end
32
+
33
+ require 'mirah/inputs'
34
+ require 'mirah/base_input_object'
35
+
36
+ Dir[File.dirname(__FILE__) + '/mirah/inputs/**/*.rb'].sort.each do |file|
37
+ require file
38
+ end
39
+
40
+ require 'mirah/client'
41
+
42
+ # Mirah ruby interoperability
43
+ module Mirah
44
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mirah
4
+ # This object provides a DSL by which you can easy create plain ruby objects with `attr_accessor` that will
5
+ # automatically be transformed back and forth from a GraphQL endpoint. This includes changing the format of the
6
+ # key from `ruby_style` to `graphqlStyle` and back again, and extra serialization where necessary for scalar types
7
+ # such as dates.
8
+ class BaseInputObject
9
+ def initialize(attrs = {})
10
+ attrs.each do |key, value|
11
+ raise Errors::InvalidParameter.new(key), "#{key} is not a valid parameter" unless respond_to? "#{key}="
12
+
13
+ send("#{key}=", value)
14
+ end
15
+ end
16
+
17
+ def valid?
18
+ self.class.inputs.all? do |input|
19
+ !input[:required] || send(input[:name])
20
+ end
21
+ end
22
+
23
+ def validate!
24
+ self.class.inputs.all? do |input|
25
+ if input[:required] && !send(input[:name])
26
+ raise Errors::MissingParameter.new(input[:name]), "#{input[:name]} is missing"
27
+ end
28
+ end
29
+ end
30
+
31
+ def to_graphql_hash
32
+ self.class.inputs.each_with_object({}) do |input, obj|
33
+ value = input[:serializer].serialize(send(input[:name]))
34
+ obj[input[:graphql_name]] = value if value
35
+ end
36
+ end
37
+
38
+ def self.from_graphql_hash(graphql_data)
39
+ return nil unless graphql_data
40
+
41
+ attrs = inputs.each_with_object({}) do |input, obj|
42
+ value = graphql_data[input[:graphql_name]]
43
+ obj[input[:name]] = input[:serializer].deserialize(value)
44
+ end
45
+
46
+ new(attrs)
47
+ end
48
+
49
+ # @private
50
+ def self.inputs
51
+ @inputs ||= []
52
+ end
53
+
54
+ # @private
55
+ def self.input(name, required:, serializer: Serializers::ScalarSerializer)
56
+ inputs.push(
57
+ { name: name,
58
+ serializer: serializer,
59
+ required: required,
60
+ graphql_name: name.to_s.camelize(:lower) }
61
+ )
62
+ attr_accessor name
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mirah
4
+ # This object provides a DSL by which you can easy create plain ruby objects with `attr_accessor` that will
5
+ # automatically be transformed back and forth from a GraphQL endpoint. This includes changing the format of the
6
+ # key from `ruby_style` to `graphqlStyle` and back again, and extra serialization where necessary for scalar types
7
+ # such as dates.
8
+ class BaseObject
9
+ def initialize(attrs = {})
10
+ attrs.each do |key, value|
11
+ send("#{key}=", value)
12
+ end
13
+ end
14
+
15
+ def to_graphql_hash
16
+ self.class.attributes.each_with_object({}) do |attribute, obj|
17
+ value = attribute[:serializer].serialize(send(attribute[:name]))
18
+ write_value(obj, value, attribute)
19
+ end
20
+ end
21
+
22
+ def self.from_graphql_hash(graphql_data)
23
+ return nil unless graphql_data
24
+
25
+ attrs = attributes.each_with_object({}) do |attribute, obj|
26
+ value = read_value(graphql_data, attribute)
27
+ obj[attribute[:name]] = attribute[:serializer].deserialize(value)
28
+ end
29
+
30
+ new(attrs)
31
+ end
32
+
33
+ # @private
34
+ def self.attributes
35
+ @attributes ||= []
36
+ end
37
+
38
+ # @private
39
+ def self.attribute(name, serializer: Serializers::ScalarSerializer, target: name, path: nil)
40
+ attributes.push(
41
+ {
42
+ name: name,
43
+ serializer: serializer,
44
+ path: path,
45
+ graphql_name: target.to_s.camelize(:lower)
46
+ }
47
+ )
48
+ attr_accessor name
49
+ end
50
+
51
+ private
52
+
53
+ def write_value(object, value, attribute)
54
+ object_to_write = object
55
+
56
+ object_to_write = write_nested_value(object, attribute[:path].dup) if attribute[:path]
57
+
58
+ object_to_write[attribute[:graphql_name]] = value if value
59
+ end
60
+
61
+ def write_nested_value(object, path)
62
+ current_obj = object
63
+
64
+ loop do
65
+ path_part = path.shift
66
+ current_obj[path_part] ||= {}
67
+ current_obj = current_obj[path_part]
68
+ break current_obj unless path&.length&.positive?
69
+ end
70
+ end
71
+
72
+ class << self
73
+ private
74
+
75
+ def read_value(graphql_data, attribute)
76
+ if attribute[:path]
77
+ read_nested_path(graphql_data, attribute[:path].dup, attribute[:graphql_name])
78
+ elsif graphql_data
79
+ graphql_data[attribute[:graphql_name]]
80
+ end
81
+ end
82
+
83
+ def read_nested_path(data, path, target)
84
+ return data[target] unless path&.length&.positive?
85
+
86
+ new_data = data[path.shift]
87
+
88
+ return nil unless new_data
89
+
90
+ if new_data.is_a? Array
91
+ new_data.map { |item| read_nested_path(item, path, target) }
92
+ else
93
+ read_nested_path(new_data, path, target)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,420 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mirah
4
+ # A client designed to communicate with the Mirah system in a constrained set of well defined ways.
5
+ # This is a front to the more general Graphql backend which is available.
6
+ #
7
+ # == Patient Methods
8
+ # * {find_patient}
9
+ # * {find_patient_by_external_id}
10
+ # * {query_patients}
11
+ # * {push_patient}
12
+ #
13
+ # == Organization Methods
14
+ # * {find_organization}
15
+ # * {find_organization_by_external_id}
16
+ # * {query_organizations}
17
+ # * {push_organization}
18
+ #
19
+ # == Practitioner Methods
20
+ # * {find_practitioner}
21
+ # * {find_practitioner_by_external_id}
22
+ # * {query_practitioners}
23
+ # * {push_practitioner}
24
+ #
25
+ # == Appointment Methods
26
+ # * {find_appointment}
27
+ # * {find_appointment_by_external_id}
28
+ # * {query_appointments}
29
+ # * {push_appointment}
30
+ #
31
+ # @example
32
+ # # Create a new client
33
+ # client = Mirah::Client.new(host: 'https://api.mirah.com', user_id: 'my_user_id', access_token: 'my_token')
34
+ #
35
+ # # find a patient
36
+ # client.find_patient_by_external_id('mrn0001')
37
+ class Client # rubocop:disable Metrics/ClassLength
38
+ # Construct a new Client with the given host and credentials.
39
+ # @param host [String] The host, e.g. 'https://api.mirah.com'
40
+ # @param user_id [String] Your user id
41
+ # @param access_token [String] Your secret API token.
42
+ def initialize(host:, user_id:, access_token:)
43
+ @client = Graphql.create_client(host: host)
44
+ @client_context = { user_id: user_id, access_token: access_token }
45
+ end
46
+
47
+ # Access to the underlying graphql client. You may use this for advanced queries that are not provided
48
+ # as part of the standard library.
49
+ attr_reader :client
50
+
51
+ #################################################################################################################
52
+ # PATIENT METHODS
53
+ #################################################################################################################
54
+
55
+ # Find a patient by the given Mirah internal UUID. This method should be used if you already know the Mirah
56
+ # identifier. If you wish to query by your own system identifier, you should use {#find_patient_by_external_id}
57
+ #
58
+ # @since 0.1.0
59
+ # @param id [String] Mirah UUID for the patient
60
+ # @return [Data::Patient, nil] the patient, or nil if the record does not exist.
61
+ def find_patient(id)
62
+ query_record(Graphql::Queries::PatientIdQuery, id, Data::Patient, 'patient')
63
+ end
64
+
65
+ # Find a patient by your external id. This is a convenience method. If you wish to query a list of patients
66
+ # by external id, please use {Client#query_patients}.
67
+ #
68
+ # @since 0.1.0
69
+ # @param external_id [String] The identifier of the system of record
70
+ # @return [Data::Patient, nil] the patient, or nil if the record does not exist.
71
+ def find_patient_by_external_id(external_id)
72
+ query_record_by_external_id(Graphql::Queries::PatientExternalIdQuery,
73
+ external_id,
74
+ Data::Patient,
75
+ 'patientExternal')
76
+ end
77
+
78
+ # Query for patients. You may specify a set of parameters as defined in {Mirah::Filters::PatientFilters}.
79
+ # Results are returned in a paginated format. See {Collection} for how to page results.
80
+ #
81
+ # @since 0.1.0
82
+ # @param external_id [Array<String>] See {Mirah::Filters::PatientFilters#external_id}
83
+ # @param search [String] See {Mirah::Filters::PatientFilters#search}
84
+ # @return [Collection<Data::Patient>] a collection of pageable patients.
85
+ def query_patients(external_id: nil, search: nil)
86
+ query_connection(
87
+ Graphql::Queries::PatientQuery,
88
+ Filters::PatientFilters.new(external_id: external_id, search: search),
89
+ Filters::Paging.default,
90
+ Data::Patient,
91
+ 'patients'
92
+ )
93
+ end
94
+
95
+ # Create or update a patient. You must specify an external identifier, all other parameters are optional,
96
+ # but you may receive errors if you attempt to specify too few parameters for a patient that does not exist.
97
+ #
98
+ # @since 0.1.0
99
+ # @param external_id [String] the external identifier for this patient
100
+ # @option input_values [String, nil] :given_name see {Data::Patient#given_name}
101
+ # @option input_values [String, nil] :family_name see {Data::Patient#family_name}
102
+ # @option input_values [Date, nil] :birth_date see {Data::Patient#birth_date}
103
+ # @option input_values [String, nil] :gender see {Data::Patient#gender}
104
+ # @option input_values [String, nil] :primary_language see {Data::Patient#primary_language}
105
+ # @option input_values [String, nil] :email see {Data::Patient#email}
106
+ # @option input_values [String, nil] :phone_number see {Data::Patient#phone_number}
107
+ # @return [PushResult<Data::Patient>] the operation result with a patient on success
108
+ def push_patient(external_id:, **input_values)
109
+ mutate(Graphql::Mutations::CreateOrUpdatePatientMutation,
110
+ Inputs::PatientInput.new(input_values.merge(external_id: external_id)),
111
+ Data::Patient, 'createOrUpdatePatient')
112
+ end
113
+
114
+ #################################################################################################################
115
+ # ORGANIZATION METHODS
116
+ #################################################################################################################
117
+
118
+ # Find an organization by the given Mirah internal UUID. This method should be used if you already know the Mirah
119
+ # identifier. If you wish to query by your own system identifier, you should use {#find_organization_by_external_id}
120
+ #
121
+ # @since 0.1.0
122
+ # @param id [String] Mirah UUID for the organization
123
+ # @return [Data::Organization, nil] the organization, or nil if the record does not exist.
124
+ def find_organization(id)
125
+ query_record(Graphql::Queries::OrganizationIdQuery, id, Data::Organization, 'organization')
126
+ end
127
+
128
+ # Find an organization by your external id. This is a convenience method. If you wish to query a list of
129
+ # organizations by external id, please use {Client#query_organizations}.
130
+ #
131
+ # @since 0.1.0
132
+ # @param external_id [String] The identifier of the system of record
133
+ # @return [Data::Organization, nil] the organization, or nil if the record does not exist.
134
+ def find_organization_by_external_id(external_id)
135
+ query_record_by_external_id(Graphql::Queries::OrganizationExternalIdQuery,
136
+ external_id,
137
+ Data::Organization,
138
+ 'organizationExternal')
139
+ end
140
+
141
+ # Query for organizations. You may specify a set of parameters as defined in {Mirah::Filters::OrganizationFilters}.
142
+ # Results are returned in a paginated format. See {Collection} for how to page results.
143
+
144
+ # @since 0.1.0
145
+ # @param external_id [Array<String>] See {Mirah::Filters::OrganizationFilters#external_id}
146
+ # @param search [String] See {Mirah::Filters::OrganizationFilters#search}
147
+ # @return [Collection<Data::Organization>] a collection of pageable organizations.
148
+ def query_organizations(external_id: nil, search: nil)
149
+ query_connection(
150
+ Graphql::Queries::OrganizationQuery,
151
+ Filters::OrganizationFilters.new(external_id: external_id, search: search),
152
+ Filters::Paging.default,
153
+ Data::Organization,
154
+ 'organizations'
155
+ )
156
+ end
157
+
158
+ # Create or update an organization. You must specify an external identifier, all other parameters are optional,
159
+ # but you may receive errors if you attempt to specify too few parameters for an organization that does not exist.
160
+ #
161
+ # @since 0.1.0
162
+ # @param external_id [String] the external identifier for this organization
163
+ # @option input_values [String, nil] :name see {Data::Organization#name}
164
+ # @option input_values [String, nil] :external_part_of_id The external identifier for the parent organization
165
+ # @return [PushResult<Data::Organization>] the operation result with the organization on success
166
+ def push_organization(external_id:, **input_values)
167
+ mutate(Graphql::Mutations::CreateOrUpdateOrganizationMutation,
168
+ Inputs::OrganizationInput.new(input_values.merge(external_id: external_id)),
169
+ Data::Organization, 'createOrUpdateOrganization')
170
+ end
171
+
172
+ #################################################################################################################
173
+ # PRACTITIONER METHODS
174
+ #################################################################################################################
175
+
176
+ # Find an practitioner by the given Mirah internal UUID. This method should be used if you already know the Mirah
177
+ # identifier. If you wish to query by your own system identifier, you should use {#find_practitioner_by_external_id}
178
+ #
179
+ # @since 0.1.0
180
+ # @param id [String] Mirah UUID for the practitioner
181
+ # @return [Data::Practitioner, nil] the practitioner, or nil if the record does not exist.
182
+ def find_practitioner(id)
183
+ query_record(Graphql::Queries::PractitionerIdQuery, id, Data::Practitioner, 'practitioner')
184
+ end
185
+
186
+ # Find an practitioner by your external id. This is a convenience method. If you wish to query a list of
187
+ # practitioners by external id, please use {Client#query_practitioners}.
188
+ #
189
+ # @since 0.1.0
190
+ # @param external_id [String] The identifier of the system of record
191
+ # @return [Data::Practitioner, nil] the practitioner, or nil if the record does not exist.
192
+ def find_practitioner_by_external_id(external_id)
193
+ query_record_by_external_id(Graphql::Queries::PractitionerExternalIdQuery,
194
+ external_id,
195
+ Data::Practitioner,
196
+ 'practitionerExternal')
197
+ end
198
+
199
+ # Query for practitioners. You may specify a set of parameters as defined in {Mirah::Filters::PractitionerFilters}.
200
+ # Results are returned in a paginated format. See {Collection} for how to page results.
201
+ #
202
+ # @since 0.1.0
203
+ # @param external_id [String] See {Mirah::Filters::PractitionerFilters#external_id}
204
+ # @param search [String] See {Mirah::Filters::PractitionerFilters#search}
205
+ # @return [Collection<Data::Practitioner>] a collection of pageable practitioners.
206
+ def query_practitioners(external_id: nil, search: nil)
207
+ query_connection(
208
+ Graphql::Queries::PractitionerQuery,
209
+ Filters::PractitionerFilters.new(external_id: external_id, search: search),
210
+ Filters::Paging.default,
211
+ Data::Practitioner,
212
+ 'practitioners'
213
+ )
214
+ end
215
+
216
+ # Create or update an practitioner. You must specify an external identifier, all other parameters are optional,
217
+ # but you may receive errors if you attempt to specify too few parameters for an practitioner that does not exist.
218
+ #
219
+ # @since 0.1.0
220
+ # @param external_id [String] the external identifier for this practitioner
221
+ # @option input_values [String, nil] :given_name see {Data::Practitioner#given_name}
222
+ # @option input_values [String, nil] :family_name see {Data::Practitioner#family_name}
223
+ # @option input_values [String, nil] :title see {Data::Practitioner#title}
224
+ # @option input_values [String, nil] :suffix see {Data::Practitioner#suffix}
225
+ # @option input_values [String, nil] :email see {Data::Practitioner#email}
226
+ # @option input_values [String, nil] :default_practitioner_role see {Data::Practitioner#default_practitioner_role}
227
+ # @option input_values [String, nil] :sso_username see {Data::Practitioner#sso_username}
228
+ # @option input_values [Array<String>, nil] :external_organization_ids see
229
+ # {Data::Practitioner#external_organization_ids}
230
+ # @return [PushResult<Data::Practitioner>] the operation result with the practitioner on success
231
+ def push_practitioner(external_id:, **input_values)
232
+ mutate(Graphql::Mutations::CreateOrUpdatePractitionerMutation,
233
+ Inputs::PractitionerInput.new(input_values.merge(external_id: external_id)),
234
+ Data::Practitioner, 'createOrUpdatePractitioner')
235
+ end
236
+
237
+ #################################################################################################################
238
+ # APPOINTMENT METHODS
239
+ #################################################################################################################
240
+
241
+ # Find an appointment by the given Mirah internal UUID. This method should be used if you already know the Mirah
242
+ # identifier. If you wish to query by your own system identifier, you should use {#find_appointment_by_external_id}
243
+ #
244
+ # @since 0.1.0
245
+ # @param id [String] Mirah UUID for the appointment
246
+ # @return [Data::Appointment, nil] the appointment, or nil if the record does not exist.
247
+ def find_appointment(id)
248
+ query_record(Graphql::Queries::AppointmentIdQuery, id, Data::Appointment, 'appointment')
249
+ end
250
+
251
+ # Find an appointment by your external id. This is a convenience method. If you wish to query a list of
252
+ # appointments by external id, please use {Client#query_appointments}.
253
+ #
254
+ # @since 0.1.0
255
+ # @param external_id [String] The identifier of the system of record
256
+ # @return [Data::Appointment, nil] the appointment, or nil if the record does not exist.
257
+ def find_appointment_by_external_id(external_id)
258
+ query_record_by_external_id(Graphql::Queries::AppointmentExternalIdQuery,
259
+ external_id,
260
+ Data::Appointment,
261
+ 'appointmentExternal')
262
+ end
263
+
264
+ # Query for appointments. You may specify a set of parameters as defined in {Mirah::Filters::AppointmentFilters}.
265
+ # Results are returned in a paginated format. See {Collection} for how to page results.
266
+
267
+ # @since 0.1.0
268
+ # @param external_id [Array<String>] See {Mirah::Filters::AppointmentFilters#external_id}
269
+ # @param status [String] See {Mirah::Filters::AppointmentFilters#external_id}
270
+ # @return [Collection<Data::Appointment>] a collection of pageable appointments.
271
+ def query_appointments(external_id: nil, status: nil)
272
+ query_connection(
273
+ Graphql::Queries::AppointmentQuery,
274
+ Filters::AppointmentFilters.new(external_id: external_id, status: status),
275
+ Filters::Paging.default,
276
+ Data::Appointment,
277
+ 'appointments'
278
+ )
279
+ end
280
+
281
+ # Create or update an appointment. You must specify an external identifier, all other parameters are optional,
282
+ # but you may receive errors if you attempt to specify too few parameters for an appointment that does not exist.
283
+ #
284
+ # @since 0.1.0
285
+ # @param external_id [String] the external identifier for this appointment
286
+ # @param status [String] the status identifier of this appointment, see {Data::Appointment#status}
287
+ # @option input_values [String, nil] :start_date see {Data::Appointment#start_date}
288
+ # @option input_values [String, nil] :end_date see {Data::Appointment#end_date}
289
+ # @option input_values [String, nil] :minutes_duration see {Data::Appointment#minutes_duration}
290
+ # @option input_values [String, nil] :patient_id see {Data::Appointment#patient_id}
291
+ # @option input_values [String, nil] :external_patient_id see {Data::Appointment#external_patient_id}
292
+ # @option input_values [String, nil] :practitioner_id see {Data::Appointment#practitioner_id}
293
+ # @option input_values [String, nil] :external_practitioner_id see {Data::Appointment#external_practitioner_id}
294
+ # @option input_values [String, nil] :organization_id see {Data::Appointment#organization_id}
295
+ # @option input_values [String, nil] :external_organization_id see {Data::Appointment#external_organization_id}
296
+ # @return [PushResult<Data::Appointment>] the operation result with the appointment on success
297
+ def push_appointment(external_id:, status:, **input_values)
298
+ mutate(Graphql::Mutations::CreateOrUpdateAppointmentMutation,
299
+ Inputs::AppointmentInput.new(input_values.merge(external_id: external_id, status: status)),
300
+ Data::Appointment, 'createOrUpdateAppointment')
301
+ end
302
+
303
+ ##################################################################################################################
304
+ # Internal methods
305
+ ##################################################################################################################
306
+
307
+ # This is technically a public method so that collections can get the next page, but should not generally
308
+ # be invoked directly otherwise.
309
+ # @private
310
+ def query_connection(query, input, paging, data_klass, path)
311
+ variables = input.to_graphql_hash.merge(paging.to_graphql_hash)
312
+ response = client.query(query, variables: variables, context: client_context)
313
+ check_response_for_errors(response)
314
+
315
+ response_json = response.to_h
316
+ results = parse_graphql_connection_response(response_json, data_klass, path, 'nodes')
317
+ page_info = parse_page_info(response_json, path)
318
+
319
+ # Used to generate subsequent pages
320
+ query_details = { query: query, input: input, paging: paging, data_klass: data_klass, path: path }
321
+ Collection.new(results: results, page_info: page_info, client: self, query: query_details)
322
+ rescue StandardError => e
323
+ handle_errors(e)
324
+ end
325
+
326
+ private
327
+
328
+ attr_reader :client_context
329
+
330
+ def query_record(query, id, data_klass, path)
331
+ response = client.query(query, variables: { id: id }, context: client_context)
332
+ check_response_for_errors(response)
333
+ response_json = response.to_h
334
+
335
+ parse_graphql_response(response_json, data_klass, path)
336
+ rescue StandardError => e
337
+ handle_errors(e)
338
+ end
339
+
340
+ def query_record_by_external_id(query, external_id, data_klass, path)
341
+ response = client.query(query, variables: { externalId: external_id }, context: client_context)
342
+ check_response_for_errors(response)
343
+ response_json = response.to_h
344
+
345
+ parse_graphql_response(response_json, data_klass, path)
346
+ rescue StandardError => e
347
+ handle_errors(e)
348
+ end
349
+
350
+ def mutate(mutation, input, data_klass, path)
351
+ input.validate!
352
+ response = client.query(mutation, variables: { input: input.to_graphql_hash }, context: client_context)
353
+
354
+ response_json = response.to_h.dig('data', path)
355
+
356
+ if response_json
357
+ result = data_klass.from_graphql_hash(response_json['result']) if response_json['result']
358
+
359
+ status = response_json['status']
360
+ errors = response_json['errors']
361
+ else
362
+ status = 'ERROR'
363
+ errors = response.to_h['errors']
364
+ end
365
+
366
+ PushResult.new(result: result, status: status, errors: errors, input: input)
367
+ rescue StandardError => e
368
+ handle_errors(e)
369
+ end
370
+
371
+ # Parse the main part of the graphql response into the appropriate data class.
372
+ #
373
+ # @param response_json [GraphQL::Client::Response] the graphql error response
374
+ # @param data_klass [Class] the class to transform the response into
375
+ # @param path [String] the path in the response to look for the raw data
376
+ # @return [Class] an item of the type of `data_klass`
377
+ def parse_graphql_response(response_json, data_klass, path)
378
+ data_klass.from_graphql_hash(response_json.dig('data', path))
379
+ end
380
+
381
+ def parse_page_info(response_json, path)
382
+ Data::PageInfo.from_graphql_hash(response_json.dig('data', path, 'pageInfo'))
383
+ end
384
+
385
+ # Parse the main part of the graphql response into the appropriate data class with additional paging
386
+ # and connection information.
387
+ #
388
+ # @param response_json [GraphQL::Client::Response] the graphql error response
389
+ # @param data_klass [Class] the class to transform the response into
390
+ # @param path [String] the path in the response to look for the raw data
391
+ # @return [Collection<Class>] a wrapped collection with the results
392
+ def parse_graphql_connection_response(response_json, data_klass, path, item = 'nodes')
393
+ response_json.dig('data', path, item)&.map do |datum|
394
+ data_klass.from_graphql_hash(datum.to_h)
395
+ end
396
+ end
397
+
398
+ # Check that any errors raised have been wrapped in Mirah errors appropriately.
399
+ def handle_errors(e)
400
+ case e
401
+ when Error
402
+ raise
403
+ else
404
+ raise Errors::ClientError, e
405
+ end
406
+ end
407
+
408
+ def check_response_for_errors(response)
409
+ if response.errors.any?
410
+ if response.errors[:data] == ['401 Unauthorized'] # rubocop:disable Style/GuardClause
411
+ raise Errors::InvalidCredentials, 'The credentials you have supplied are invalid'
412
+ else
413
+ raise Errors::ServerError, 'Unknown error from Mirah server: ' + response.errors.values.flatten.join(',')
414
+ end
415
+ end
416
+
417
+ raise Errors::ServerError, 'Data payload was empty' unless response.data
418
+ end
419
+ end
420
+ end