mirah-ruby 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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