data_nexus 0.2.2 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 27c91c699abd23dd3463ae2dbe82840f32cb2d0dc8753caec22734c86fda1fd5
4
- data.tar.gz: 85bf87c4cad24a20ae307b996cdafbcc5827a3f0f8b1f375b4b506d7a28d0715
3
+ metadata.gz: 11502860850ccde0aa9b28ec2770d583d716af03d2d8a2cba9beb60383933dae
4
+ data.tar.gz: a9e2a9215fb714a6a015d629de69dc112d55697f8d1c227a61689b2f8fbe3c56
5
5
  SHA512:
6
- metadata.gz: fb51987bebd0709461f56093be6c0d3b4ab9dbb151f7f2c5db8c6399050d5f8cf168d1ae4e5bf3b31129ccc838e118c1dce23df78fd41f2c8a3eb0058eed23a3
7
- data.tar.gz: 7ff0c3784d400e5b28e2fd1ea2d51e27162d8fa759fbda03b05b9aa21e328401a0cb88f5173dfa3c4e8e0372f5c48872c1870bdcff0b34c9d3cf0fc4ca0ab4f5
6
+ metadata.gz: af48c59e9c0f360b3ec058721e80cfc36f7c6067420097c86c60129d7455e92c06601185e5a40df232b2d1b2c08a05137ed2231b470c8defb52c7eddf65246ed
7
+ data.tar.gz: e6f73e656deb92723c946d49299019081eedddacb866e608c961107cf92421396f790ca2329a00abd9223b06e5a9c5a364b326cba79a5e4ae8d3cbc530d2a0e2
@@ -23,7 +23,7 @@ jobs:
23
23
  persist-credentials: false
24
24
 
25
25
  - name: Set up Ruby ${{ matrix.ruby-version }}
26
- uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
26
+ uses: ruby/setup-ruby@e8944e80fb94b20106697132f8c20c665fab29e9 # v1.325.0
27
27
  with:
28
28
  ruby-version: ${{ matrix.ruby-version }}
29
29
  bundler-cache: true
@@ -40,7 +40,7 @@ jobs:
40
40
  persist-credentials: false
41
41
 
42
42
  - name: Set up Ruby
43
- uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
43
+ uses: ruby/setup-ruby@e8944e80fb94b20106697132f8c20c665fab29e9 # v1.325.0
44
44
  with:
45
45
  ruby-version: "3.2"
46
46
  bundler-cache: true
@@ -37,7 +37,7 @@ jobs:
37
37
  # cache, so a restored cache could hand the release poisoned
38
38
  # dependencies. A fresh install costs seconds.
39
39
  - name: Set up Ruby
40
- uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
40
+ uses: ruby/setup-ruby@e8944e80fb94b20106697132f8c20c665fab29e9 # v1.325.0
41
41
  with:
42
42
  ruby-version: "3.2"
43
43
 
data/AGENTS.md CHANGED
@@ -14,6 +14,7 @@
14
14
  - `collection.rb` - Collection handling
15
15
  - `errors.rb` - Custom error classes
16
16
  - `version.rb` - Gem version (bump here for releases)
17
+ - `CHANGELOG.md` - User-facing changes per release
17
18
  - `spec/` - RSpec tests
18
19
  - `.github/workflows/` - CI and release automation
19
20
 
@@ -35,11 +36,13 @@ Pin every action to a full commit SHA, with the exact version in a comment; `pin
35
36
 
36
37
  ## Releasing
37
38
 
38
- 1. Bump the version in `lib/data_nexus/version.rb`
39
+ 1. Bump the version in `lib/data_nexus/version.rb`. In `CHANGELOG.md`, rename `[Unreleased]` to `[X.Y.Z] - YYYY-MM-DD`, add a new empty `## [Unreleased]` section above it, and update the links at the bottom: `[Unreleased]` compares `vX.Y.Z...HEAD`, and a new `[X.Y.Z]` link compares the previous tag to `vX.Y.Z`
39
40
  2. Merge to `main`
40
- 3. Create a GitHub Release with tag `vX.Y.Z` matching the version
41
+ 3. Create a GitHub Release with tag `vX.Y.Z` matching the version. Paste that version's `CHANGELOG.md` section above GitHub's generated pull request list
41
42
  4. The release workflow runs tests, verifies the version/tag match, builds, and pushes to RubyGems
42
43
 
44
+ Any pull request that changes shipped behavior (anything under `lib/`, or runtime dependencies in the gemspec) adds an entry under `[Unreleased]` in `CHANGELOG.md`.
45
+
43
46
  ## When to Suggest a Release
44
47
 
45
48
  Only suggest a release when there are meaningful changes to the gem's shipped code (anything under `lib/`). Changes that do NOT warrant a release on their own:
data/CHANGELOG.md ADDED
@@ -0,0 +1,69 @@
1
+ # Changelog
2
+
3
+ Notable changes to the `data_nexus` gem. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the gem uses [Semantic Versioning](https://semver.org/).
4
+
5
+ Dependency bumps, CI changes and test-only changes are left out. From 0.2.0 on, the [GitHub releases](https://github.com/DartHealth/datanexus-ruby/releases) list every merged pull request.
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.3.0] - 2026-10-02
10
+
11
+ ### Added
12
+
13
+ - `client.programs.list` lists the programs your API key can see, sorted by name. Each program has an `:id` and a `:name`. It pages with `first`, `after`, `before` and `last` (25 programs per page by default), and `name:` filters by program name, ignoring case, so an app can look up a program ID without leaving the gem ([sc-6843](https://app.shortcut.com/dart/story/6843)):
14
+
15
+ ```ruby
16
+ program = client.programs.list(name: 'Example Program').data.first
17
+ raise "Program not found: Example Program" unless program
18
+
19
+ client.programs(program[:id]).search_members(born_on: '1980-01-15', employee_id: 'EMP123')
20
+ ```
21
+
22
+ - `Collection#next_page?` and `#previous_page?` return `false` when the response's `has_next_page` or `has_previous_page` is `false`, so iterating a list that sends those flags (such as `client.programs.list`) stops without requesting an empty page.
23
+
24
+ ### Changed
25
+
26
+ - `client.programs` with no argument now returns the program list instead of raising `ArgumentError`. `client.programs('program-id')` is unchanged.
27
+ - `client.programs(nil)` also returns the program list. Code that passes a missing program ID now fails in the gem with `NoMethodError` (for example on `.members`) instead of sending a request to `/api/programs//members`.
28
+
29
+ ### Fixed
30
+
31
+ - `Collection#next_page` and `#previous_page` return `nil` when the API sends back the same page, so `each`, `each_record` and `each_page` stop instead of looping forever. Program member lists (`client.programs('program-id').members.list`) were affected: the API returns them as a single page that still carries a cursor. Iterating one now stops after that page.
32
+
33
+ ## [0.2.2] - 2026-09-18
34
+
35
+ ### Changed
36
+
37
+ - Releases are published to RubyGems with trusted publishing. The gem's code is unchanged from 0.2.1.
38
+
39
+ ## [0.2.1] - 2026-02-04
40
+
41
+ ### Fixed
42
+
43
+ - The default `base_url` is now `https://datanexus.darthealth.com`. It was `https://api.datanexus.com`.
44
+
45
+ ## [0.2.0] - 2026-02-04
46
+
47
+ ### Added
48
+
49
+ - `client.programs('program-id').search_members(...)` searches a program's members by date of birth plus name, name prefix or employee ID. It returns up to 10 results and a `more_results` flag, and raises `ArgumentError` for an invalid parameter combination.
50
+
51
+ ## [0.1.0] - 2026-01-14
52
+
53
+ Tagged but not published to RubyGems. 0.2.0 was the first published version and includes everything below.
54
+
55
+ ### Added
56
+
57
+ - `DataNexus::Client` with API key authentication, timeouts, SSL verification and automatic retries.
58
+ - Members: `client.members.list`, `find` and `update`.
59
+ - Program members: `client.programs('program-id').members.list`, and `find`, `update` and `household` on a single member.
60
+ - Member consents and enrollments: `create`, `find`, `update` and `delete`.
61
+ - `DataNexus::Collection` for cursor-paginated lists (`next_page`, `previous_page`, `each_page`, `each`).
62
+ - Error classes for API errors (`AuthenticationError`, `NotFoundError`, `RateLimitError` and others) and connection errors.
63
+
64
+ [Unreleased]: https://github.com/DartHealth/datanexus-ruby/compare/v0.3.0...HEAD
65
+ [0.3.0]: https://github.com/DartHealth/datanexus-ruby/compare/v0.2.2...v0.3.0
66
+ [0.2.2]: https://github.com/DartHealth/datanexus-ruby/compare/v0.2.1...v0.2.2
67
+ [0.2.1]: https://github.com/DartHealth/datanexus-ruby/compare/v0.2.0...v0.2.1
68
+ [0.2.0]: https://github.com/DartHealth/datanexus-ruby/compare/v0.1.0...v0.2.0
69
+ [0.1.0]: https://github.com/DartHealth/datanexus-ruby/tree/v0.1.0
data/README.md CHANGED
@@ -17,6 +17,36 @@ client = DataNexus::Client.new(
17
17
  )
18
18
  ```
19
19
 
20
+ ## Programs
21
+
22
+ ### List Programs
23
+
24
+ Lists the programs your API key can see, sorted by name. Each program has an `:id` and a `:name`.
25
+
26
+ ```ruby
27
+ collection = client.programs.list
28
+
29
+ collection.data.each do |program|
30
+ puts "#{program[:id]} #{program[:name]}"
31
+ end
32
+ ```
33
+
34
+ A page holds 25 programs by default. `list` takes `first`, `after`, `before` and `last`, and `each` reads every page. See [Pagination](#pagination).
35
+
36
+ ### Look Up a Program by Name
37
+
38
+ Every program-scoped call needs a program ID. To start from a program's name, filter by `name`. The match ignores case and surrounding whitespace:
39
+
40
+ ```ruby
41
+ program = client.programs.list(name: 'Example Program').data.first
42
+ raise "Program not found: Example Program" unless program
43
+
44
+ client.programs(program[:id]).search_members(
45
+ born_on: '1980-01-15',
46
+ employee_id: 'EMP123'
47
+ )
48
+ ```
49
+
20
50
  ## Program Members
21
51
 
22
52
  ### List Members
@@ -37,6 +67,8 @@ collection.data.each do |member|
37
67
  end
38
68
  ```
39
69
 
70
+ Note: Program member lists return a single page of up to 25 members. See [Pagination](#pagination).
71
+
40
72
  ### Search Members
41
73
 
42
74
  Search for members within a program. Returns a bounded result set (max 10 results) with a `more_results` flag indicating if additional matches exist.
@@ -76,26 +108,10 @@ result = client.programs('program-id').search_members(
76
108
  )
77
109
  ```
78
110
 
79
- Note: Unlike `list`, `search_members` does not support pagination. It returns up to 10 results with a `more_results` boolean. An `ArgumentError` will be raised if an invalid parameter combination is provided.
111
+ Note: `search_members` does not support pagination. It returns up to 10 results with a `more_results` boolean. An `ArgumentError` will be raised if an invalid parameter combination is provided.
80
112
 
81
113
  Note: Depending on your API key, `search_members` may be the only method you have access to. Contact your DataNexus representative for more information about your API key's permissions.
82
114
 
83
- ### Pagination
84
-
85
- ```ruby
86
- collection.each_page do |page|
87
- page.data.each { |member| process(member) }
88
- end
89
-
90
- # Or iterate all records directly
91
- collection.each { |member| process(member) }
92
-
93
- # Manual pagination
94
- if collection.next_page?
95
- next_collection = collection.next_page
96
- end
97
- ```
98
-
99
115
  ### Find Member
100
116
 
101
117
  ```ruby
@@ -227,6 +243,28 @@ response = client.members.update('member-id',
227
243
  )
228
244
  ```
229
245
 
246
+ ## Pagination
247
+
248
+ `list` methods return a `DataNexus::Collection`, one page of records plus cursors. Program lists (`client.programs.list`) and top-level member lists (`client.members.list`) page with `first`, `after`, `before` and `last`:
249
+
250
+ ```ruby
251
+ collection = client.members.list(first: 50)
252
+
253
+ collection.each_page do |page|
254
+ page.data.each { |member| process(member) }
255
+ end
256
+
257
+ # Or iterate all records directly
258
+ collection.each { |member| process(member) }
259
+
260
+ # Manual pagination
261
+ if collection.next_page?
262
+ next_collection = collection.next_page
263
+ end
264
+ ```
265
+
266
+ Note: Program member lists (`client.programs('program-id').members.list`) currently return a single page of up to 25 records. `each` and `each_page` stop after that page. `next_page?` can still return `true` for these lists, and `next_page` then returns `nil`.
267
+
230
268
  ## Error Handling
231
269
 
232
270
  ```ruby
@@ -3,6 +3,7 @@
3
3
  require_relative 'configuration'
4
4
  require_relative 'connection'
5
5
  require_relative 'resources/members'
6
+ require_relative 'resources/program_list'
6
7
  require_relative 'resources/programs'
7
8
 
8
9
  module DataNexus
@@ -15,6 +16,9 @@ module DataNexus
15
16
  # config = DataNexus::Configuration.new(api_key: "your_api_key")
16
17
  # client = DataNexus::Client.new(config: config)
17
18
  #
19
+ # @example List programs
20
+ # client.programs.list.each { |program| puts "#{program[:id]} #{program[:name]}" }
21
+ #
18
22
  # @example Access program members
19
23
  # client.programs("program-uuid").members.list
20
24
  # client.programs("program-uuid").members("member-id").find
@@ -60,15 +64,26 @@ module DataNexus
60
64
  Resources::Members.new(connection)
61
65
  end
62
66
 
63
- # Access program-scoped resources
67
+ # Access programs
64
68
  #
65
- # @param program_id [String] The program UUID
66
- # @return [Resources::Programs] A program resource proxy
69
+ # When called without an argument, returns a resource for listing programs.
70
+ # When called with a program_id, returns a proxy for program-scoped resources.
67
71
  #
68
- # @example
72
+ # @param program_id [String, nil] The program UUID (optional)
73
+ # @return [Resources::ProgramList] When no program_id provided - for listing programs
74
+ # @return [Resources::Programs] When program_id provided - a program resource proxy
75
+ #
76
+ # @example List programs
77
+ # client.programs.list
78
+ #
79
+ # @example Access program members
69
80
  # client.programs("uuid").members.list
70
- def programs(program_id)
71
- Resources::Programs.new(connection, program_id)
81
+ def programs(program_id = nil)
82
+ if program_id
83
+ Resources::Programs.new(connection, program_id)
84
+ else
85
+ Resources::ProgramList.new(connection)
86
+ end
72
87
  end
73
88
 
74
89
  private
@@ -7,18 +7,18 @@ module DataNexus
7
7
  # The underlying data remains as hashes - this class just adds pagination helpers.
8
8
  #
9
9
  # @example Accessing data
10
- # collection = client.programs('uuid').members.list
10
+ # collection = client.members.list
11
11
  # collection.data.each { |member| puts member[:first_name] }
12
12
  #
13
13
  # @example Manual pagination
14
- # collection = client.programs('uuid').members.list(first: 50)
14
+ # collection = client.members.list(first: 50)
15
15
  # while collection
16
16
  # process(collection.data)
17
17
  # collection = collection.next_page
18
18
  # end
19
19
  #
20
20
  # @example Block pagination
21
- # client.programs('uuid').members.list(first: 50).each_page do |page|
21
+ # client.members.list(first: 50).each_page do |page|
22
22
  # page.data.each { |member| puts member[:first_name] }
23
23
  # end
24
24
  #
@@ -41,40 +41,60 @@ module DataNexus
41
41
  @data = response[:data] || []
42
42
  @start_cursor = response[:start_cursor]
43
43
  @end_cursor = response[:end_cursor]
44
+ @has_next_page = response[:has_next_page]
45
+ @has_previous_page = response[:has_previous_page]
44
46
  @resource = resource
45
47
  @params = params
46
48
  end
47
49
 
48
50
  # Check if there's a next page of results
49
51
  #
52
+ # Uses the response's has_next_page flag when the endpoint sends one,
53
+ # and otherwise assumes a page with an end_cursor may have a next page.
54
+ #
50
55
  # @return [Boolean]
51
56
  def next_page?
57
+ return false if @has_next_page == false
58
+
52
59
  !end_cursor.nil? && !end_cursor.empty?
53
60
  end
54
61
 
55
62
  # Fetch the next page of results
56
63
  #
64
+ # Returns nil if the API responds with this same page again (an endpoint
65
+ # that ignores the cursor), so iterating all pages always terminates.
66
+ #
57
67
  # @return [Collection, nil] The next page, or nil if no more pages
58
68
  def next_page
59
69
  return nil unless next_page?
60
70
 
61
- @resource.list(**@params, after: end_cursor)
71
+ page = @resource.list(**@params, after: end_cursor)
72
+ page unless page&.end_cursor == end_cursor
62
73
  end
63
74
 
64
75
  # Check if there's a previous page of results
65
76
  #
77
+ # Uses the response's has_previous_page flag when the endpoint sends one,
78
+ # and otherwise assumes a page with a start_cursor may have a previous page.
79
+ #
66
80
  # @return [Boolean]
67
81
  def previous_page?
82
+ return false if @has_previous_page == false
83
+
68
84
  !start_cursor.nil? && !start_cursor.empty?
69
85
  end
70
86
 
71
87
  # Fetch the previous page of results
72
88
  #
89
+ # Returns nil if the API responds with this same page again (an endpoint
90
+ # that ignores the cursor).
91
+ #
73
92
  # @return [Collection, nil] The previous page, or nil if no more pages
74
93
  def previous_page
75
94
  return nil unless previous_page?
76
95
 
77
- @resource.list(**@params, before: start_cursor)
96
+ page = @resource.list(**@params, before: start_cursor)
97
+ page unless page&.start_cursor == start_cursor
78
98
  end
79
99
 
80
100
  # Iterate through all pages starting from this one
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DataNexus
4
+ module Resources
5
+ # Resource for listing the programs visible to the API key
6
+ #
7
+ # For operations on a specific program, use `programs("uuid")`.
8
+ #
9
+ # @example List programs
10
+ # collection = client.programs.list
11
+ # collection.data.each { |program| puts "#{program[:id]} #{program[:name]}" }
12
+ #
13
+ # @example Look up a program ID by name
14
+ # program = client.programs.list(name: "Example Program").data.first
15
+ # raise "Program not found" unless program
16
+ # client.programs(program[:id]).search_members(born_on: "1976-07-04", employee_id: "ABC123")
17
+ #
18
+ class ProgramList
19
+ # @return [Connection] The HTTP connection
20
+ attr_reader :connection
21
+
22
+ # Initialize a new ProgramList resource
23
+ #
24
+ # @param connection [Connection] The HTTP connection
25
+ def initialize(connection)
26
+ @connection = connection
27
+ end
28
+
29
+ # List programs, sorted by name
30
+ #
31
+ # A page holds 25 programs unless you pass `first` or `last`.
32
+ #
33
+ # @param name [String, nil] Return only programs with this name, ignoring case
34
+ # @param after [String, nil] Cursor for next group of records
35
+ # @param before [String, nil] Cursor for previous group of records
36
+ # @param first [Integer, nil] Number of records to fetch after cursor
37
+ # @param last [Integer, nil] Number of records to fetch before cursor
38
+ #
39
+ # @return [Collection] Collection of programs, each with :id and :name
40
+ #
41
+ # @example Basic listing
42
+ # collection = client.programs.list
43
+ # collection.data.each { |p| puts p[:name] }
44
+ #
45
+ # @example Filter by name
46
+ # collection = client.programs.list(name: "Example Program")
47
+ #
48
+ # @example Iterate over every program, across pages
49
+ # client.programs.list.each { |p| puts p[:name] }
50
+ def list(**params)
51
+ allowed_params = %i[after before first last name]
52
+
53
+ query_params = params.slice(*allowed_params).compact
54
+ response = connection.get(base_path, query_params)
55
+ Collection.new(response, resource: self, params: query_params)
56
+ end
57
+
58
+ private
59
+
60
+ # Base path for programs endpoints
61
+ #
62
+ # @return [String]
63
+ def base_path
64
+ '/api/programs'
65
+ end
66
+ end
67
+ end
68
+ end
@@ -7,16 +7,15 @@ module DataNexus
7
7
  # Provides methods for listing and searching members within a specific program.
8
8
  # For operations on a specific member, use `programs("uuid").members("member-id")`.
9
9
  #
10
+ # Note: `list` currently returns a single page of up to 25 members, and
11
+ # iteration stops after that page.
12
+ #
10
13
  # @example List members with filters
11
14
  # client.programs("uuid").members.list(
12
15
  # first_name: "george",
13
16
  # born_on: "1976-07-04"
14
17
  # )
15
18
  #
16
- # @example Paginate through members
17
- # collection = client.programs("uuid").members.list(first: 50)
18
- # collection.each_page { |page| process(page.data) }
19
- #
20
19
  class ProgramMembers
21
20
  # @return [Connection] The HTTP connection
22
21
  attr_reader :connection
@@ -42,7 +41,7 @@ module DataNexus
42
41
  # @param born_on [String, nil] Filter by date of birth (YYYY-MM-DD)
43
42
  # @param employee_id [String, nil] Filter by employee ID
44
43
  #
45
- # @return [Collection] Paginated collection of members
44
+ # @return [Collection] The first page of matching members
46
45
  #
47
46
  # @example Basic listing
48
47
  # collection = client.programs("uuid").members.list
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DataNexus
4
- VERSION = '0.2.2'
4
+ VERSION = '0.3.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: data_nexus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Kibler
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-18 00:00:00.000000000 Z
11
+ date: 2026-10-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -54,6 +54,7 @@ files:
54
54
  - ".mise.toml"
55
55
  - ".rubocop.yml"
56
56
  - AGENTS.md
57
+ - CHANGELOG.md
57
58
  - CLAUDE.md
58
59
  - README.md
59
60
  - Rakefile
@@ -66,6 +67,7 @@ files:
66
67
  - lib/data_nexus/resources/member_consents.rb
67
68
  - lib/data_nexus/resources/member_enrollments.rb
68
69
  - lib/data_nexus/resources/members.rb
70
+ - lib/data_nexus/resources/program_list.rb
69
71
  - lib/data_nexus/resources/program_member.rb
70
72
  - lib/data_nexus/resources/program_members.rb
71
73
  - lib/data_nexus/resources/programs.rb
@@ -77,7 +79,7 @@ licenses:
77
79
  metadata:
78
80
  homepage_uri: https://github.com/DartHealth/datanexus-ruby
79
81
  source_code_uri: https://github.com/DartHealth/datanexus-ruby
80
- changelog_uri: https://github.com/DartHealth/datanexus-ruby/releases
82
+ changelog_uri: https://github.com/DartHealth/datanexus-ruby/blob/main/CHANGELOG.md
81
83
  rubygems_mfa_required: 'true'
82
84
  post_install_message:
83
85
  rdoc_options: []