data_nexus 0.2.1 → 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: 0570527fcabb635362f5f477c6b4c28c24f19132b984fcd657e33719a3fda846
4
- data.tar.gz: 149667e5730ab383c7b729f80167791498f4cf56a9a2fec1a8b7e4a96ad981a7
3
+ metadata.gz: 11502860850ccde0aa9b28ec2770d583d716af03d2d8a2cba9beb60383933dae
4
+ data.tar.gz: a9e2a9215fb714a6a015d629de69dc112d55697f8d1c227a61689b2f8fbe3c56
5
5
  SHA512:
6
- metadata.gz: 1e50da78e344a456caa86aee7ba292cc225601a4fb8c1f35a10ec02efb9c2ad392b0037d004c7544707cdf8ddb8074294d0cc2483963d210190ff33c2c986797
7
- data.tar.gz: 419e0ea79cf1fb25d7f503d50a1ee70ddf8641b6865d2b803da6f6a1581b4719e1e424503d3f84099ccfc917f144d0615a68ac957fa80a31ae1ff4e474d7e8f6
6
+ metadata.gz: af48c59e9c0f360b3ec058721e80cfc36f7c6067420097c86c60129d7455e92c06601185e5a40df232b2d1b2c08a05137ed2231b470c8defb52c7eddf65246ed
7
+ data.tar.gz: e6f73e656deb92723c946d49299019081eedddacb866e608c961107cf92421396f790ca2329a00abd9223b06e5a9c5a364b326cba79a5e4ae8d3cbc530d2a0e2
@@ -0,0 +1,17 @@
1
+ {
2
+ "problemMatcher": [
3
+ {
4
+ "owner": "actionlint",
5
+ "pattern": [
6
+ {
7
+ "regexp": "^(?:\\x1b\\[\\d+m)?(.+?)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*: (?:\\x1b\\[\\d+m)*(.+?)(?:\\x1b\\[\\d+m)* \\[(.+?)\\]$",
8
+ "file": 1,
9
+ "line": 2,
10
+ "column": 3,
11
+ "message": 4,
12
+ "code": 5
13
+ }
14
+ ]
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,21 @@
1
+ # To get started with Dependabot version updates, you'll need to specify which
2
+ # package ecosystems to update and where the package manifests are located.
3
+ # Please see the documentation for all configuration options:
4
+ # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5
+
6
+ version: 2
7
+ updates:
8
+ - package-ecosystem: "bundler" # See documentation for possible values
9
+ directory: "/" # Location of package manifests
10
+ schedule:
11
+ interval: "weekly"
12
+ cooldown:
13
+ default-days: 7
14
+ # Keeps the SHA-pinned actions in .github/workflows current (SC-6615).
15
+ # Without this entry the pins would never move.
16
+ - package-ecosystem: "github-actions"
17
+ directory: "/"
18
+ schedule:
19
+ interval: "weekly"
20
+ cooldown:
21
+ default-days: 7
@@ -1,5 +1,7 @@
1
1
  name: CI
2
2
 
3
+ permissions:
4
+ contents: read
3
5
  on:
4
6
  push:
5
7
  branches: [main]
@@ -16,10 +18,12 @@ jobs:
16
18
  ruby-version: ["3.0", "3.1", "3.2", "3.4", "4.0"]
17
19
 
18
20
  steps:
19
- - uses: actions/checkout@v4
21
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
22
+ with:
23
+ persist-credentials: false
20
24
 
21
25
  - name: Set up Ruby ${{ matrix.ruby-version }}
22
- uses: ruby/setup-ruby@v1
26
+ uses: ruby/setup-ruby@e8944e80fb94b20106697132f8c20c665fab29e9 # v1.325.0
23
27
  with:
24
28
  ruby-version: ${{ matrix.ruby-version }}
25
29
  bundler-cache: true
@@ -31,10 +35,12 @@ jobs:
31
35
  runs-on: ubuntu-latest
32
36
 
33
37
  steps:
34
- - uses: actions/checkout@v4
38
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
39
+ with:
40
+ persist-credentials: false
35
41
 
36
42
  - name: Set up Ruby
37
- uses: ruby/setup-ruby@v1
43
+ uses: ruby/setup-ruby@e8944e80fb94b20106697132f8c20c665fab29e9 # v1.325.0
38
44
  with:
39
45
  ruby-version: "3.2"
40
46
  bundler-cache: true
@@ -0,0 +1,80 @@
1
+ name: GitHub Actions Scan
2
+
3
+ # Security (zizmor) and correctness (actionlint) scanning of this repo's own
4
+ # GitHub Actions config (SC-6615). Each job fails on any finding. zizmor covers
5
+ # workflows, composite actions and dependabot.yml; actionlint covers workflows
6
+ # only -- it does not lint the steps of composite actions.
7
+ #
8
+ # Results arrive as annotations on each job's check run (inline on the diff for
9
+ # a PR). GitHub renders at most 10 annotations per step, so the job log is the
10
+ # complete list. The job still fails on any finding, however many are rendered.
11
+ #
12
+ # No path filter, on purpose: a path-filtered workflow never reports on a PR
13
+ # that doesn't touch .github/, so it could never be made a required check.
14
+ # Both jobs finish in well under a minute.
15
+ #
16
+ # Run both locally before pushing a workflow change:
17
+ # GH_TOKEN=$(gh auth token) zizmor . # without a token, online audits are skipped
18
+ # actionlint
19
+ # To suppress a finding, put `# zizmor: ignore[<audit>]` with a reason on the
20
+ # offending line. Disable an audit repo-wide (.github/zizmor.yml) only when it
21
+ # is wrong for the whole repo.
22
+ #
23
+ # The same file runs in every DartHealth repo with workflows (SC-6615);
24
+ # DartHealth/datahub holds the rollout notes. Keep the copies in step.
25
+
26
+ on:
27
+ pull_request:
28
+ push:
29
+ branches:
30
+ - main
31
+
32
+ permissions:
33
+ contents: read
34
+
35
+ jobs:
36
+ zizmor:
37
+ name: zizmor
38
+ runs-on: ubuntu-latest
39
+ timeout-minutes: 5
40
+ steps:
41
+ - name: Checkout repository
42
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
43
+ with:
44
+ persist-credentials: false
45
+
46
+ - name: Run zizmor
47
+ uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
48
+ with:
49
+ # Annotations, not SARIF. In SARIF mode zizmor exits 0 whatever it
50
+ # finds, so `advanced-security: true` would quietly turn this gate
51
+ # into an advisory check -- even on a repo that has Code Scanning.
52
+ advanced-security: false
53
+ annotations: true
54
+ # `version` is left at its default, `latest`, which the action
55
+ # resolves to the digest-pinned zizmor image it shipped with. So the
56
+ # SHA pin above also pins zizmor, and a Dependabot bump of this action
57
+ # is the zizmor upgrade.
58
+
59
+ actionlint:
60
+ name: actionlint
61
+ runs-on: ubuntu-latest
62
+ timeout-minutes: 5
63
+ steps:
64
+ - name: Checkout repository
65
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
66
+ with:
67
+ persist-credentials: false
68
+
69
+ # Turns actionlint's output into PR annotations. The matcher file is
70
+ # upstream's, copied verbatim at the same version as the image below.
71
+ - name: Register actionlint problem matcher
72
+ run: echo "::add-matcher::.github/actionlint-matcher.json"
73
+
74
+ # The image bundles shellcheck, so `run:` blocks are checked too. This is
75
+ # a digest-pinned docker:// reference, which Dependabot may not bump:
76
+ # update it by hand, and re-copy the matcher from the same tag.
77
+ - name: Run actionlint
78
+ uses: docker://rhysd/actionlint:1.7.12@sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667
79
+ with:
80
+ args: -color
@@ -1,5 +1,8 @@
1
1
  name: Release
2
2
 
3
+ permissions:
4
+ contents: read
5
+
3
6
  on:
4
7
  release:
5
8
  types: [published]
@@ -8,14 +11,38 @@ jobs:
8
11
  release:
9
12
  runs-on: ubuntu-latest
10
13
 
14
+ # OIDC trusted publishing: id-token lets us mint a short-lived, gem-scoped
15
+ # RubyGems credential instead of a stored API key. No contents: write is
16
+ # needed -- this job builds and pushes the gem but does not push a git tag
17
+ # (the tag already exists; this workflow is triggered by release:published).
18
+ #
19
+ # No GitHub Environment / approval gate is configured (SC-6688). Trusted
20
+ # publishing already scopes publishing to this repo + workflow, which closes
21
+ # the finding on its own. An environment would add a human-approval gate on
22
+ # publishing; deferred as defense-in-depth. To add it later: register the
23
+ # environment name on the gem's trusted publisher at rubygems.org, add an
24
+ # `environment:` key here, and create a matching environment (with required
25
+ # reviewers) in the repo settings.
26
+ permissions:
27
+ contents: read
28
+ id-token: write
29
+
11
30
  steps:
12
- - uses: actions/checkout@v4
31
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
32
+ with:
33
+ persist-credentials: false
13
34
 
35
+ # No bundler cache in this job (zizmor cache-poisoning). This job
36
+ # publishes the gem, and any workflow run on this repo can write the
37
+ # cache, so a restored cache could hand the release poisoned
38
+ # dependencies. A fresh install costs seconds.
14
39
  - name: Set up Ruby
15
- uses: ruby/setup-ruby@v1
40
+ uses: ruby/setup-ruby@e8944e80fb94b20106697132f8c20c665fab29e9 # v1.325.0
16
41
  with:
17
42
  ruby-version: "3.2"
18
- bundler-cache: true
43
+
44
+ - name: Install dependencies
45
+ run: bundle install
19
46
 
20
47
  - name: Run tests
21
48
  run: bundle exec rspec
@@ -35,10 +62,17 @@ jobs:
35
62
  run: |
36
63
  gem build data_nexus.gemspec
37
64
  GEM_FILE=$(ls data_nexus-*.gem)
38
- echo "gem_file=$GEM_FILE" >> $GITHUB_OUTPUT
65
+ echo "gem_file=$GEM_FILE" >> "$GITHUB_OUTPUT"
39
66
  echo "Built: $GEM_FILE"
40
67
 
68
+ # Exchanges the GitHub OIDC token for a short-lived RubyGems credential
69
+ # via trusted publishing, so no API key is stored. Requires a trusted
70
+ # publisher registered for the data_nexus gem on rubygems.org
71
+ # (repo DartHealth/datanexus-ruby, workflow release.yml).
72
+ - name: Configure RubyGems credentials
73
+ uses: rubygems/configure-rubygems-credentials@dc5a8d8553e6ee01fc26761a49e99e733d17954a # v2.1.0
74
+
41
75
  - name: Push to RubyGems
42
- run: gem push ${{ steps.build.outputs.gem_file }}
76
+ run: gem push "$GEM_FILE"
43
77
  env:
44
- GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
78
+ GEM_FILE: ${{ steps.build.outputs.gem_file }}
data/AGENTS.md ADDED
@@ -0,0 +1,62 @@
1
+ # Agents Guide
2
+
3
+ ## Project Overview
4
+
5
+ `data_nexus` is a Ruby client gem for Dart Health's Data Nexus API. It wraps the API using Faraday and provides resource-based access.
6
+
7
+ ## Structure
8
+
9
+ - `lib/data_nexus/` - Main gem code
10
+ - `client.rb` - Entry point for API interactions
11
+ - `configuration.rb` - Config (API keys, base URL, etc.)
12
+ - `connection.rb` - Faraday HTTP connection setup
13
+ - `resources/` - API resource classes
14
+ - `collection.rb` - Collection handling
15
+ - `errors.rb` - Custom error classes
16
+ - `version.rb` - Gem version (bump here for releases)
17
+ - `CHANGELOG.md` - User-facing changes per release
18
+ - `spec/` - RSpec tests
19
+ - `.github/workflows/` - CI and release automation
20
+
21
+ ## Development
22
+
23
+ - Ruby >= 3.0.0
24
+ - Run `bundle install` to install dependencies
25
+ - Run `bundle exec rspec` to run tests
26
+ - Run `bundle exec rubocop` to lint
27
+
28
+ ## GitHub Actions scanning
29
+
30
+ `.github/workflows/github-actions-scan.yml` runs zizmor (security) and actionlint (correctness) on every PR and on every push to `main`, and fails on any finding. Before pushing a workflow change, run both locally:
31
+
32
+ - `GH_TOKEN=$(gh auth token) zizmor .` (without a token, zizmor skips its online audits)
33
+ - `actionlint`
34
+
35
+ Pin every action to a full commit SHA, with the exact version in a comment; `pinact run` does this. Dependabot keeps the pins current. To suppress a finding, add `# zizmor: ignore[<audit>]` with a reason on the offending line.
36
+
37
+ ## Releasing
38
+
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`
40
+ 2. Merge to `main`
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
42
+ 4. The release workflow runs tests, verifies the version/tag match, builds, and pushes to RubyGems
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
+
46
+ ## When to Suggest a Release
47
+
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:
49
+ - Dependabot dependency bumps (dev dependencies, Gemfile.lock-only changes)
50
+ - CI/workflow config changes
51
+ - Test-only changes
52
+ - Documentation updates
53
+
54
+ Changes that DO warrant a release:
55
+ - Bug fixes in `lib/`
56
+ - New features or API changes
57
+ - Updates to runtime dependencies in the gemspec (e.g., bumping `faraday` constraint)
58
+
59
+ ## Key Dependencies
60
+
61
+ - `faraday` (~> 2.0) - HTTP client
62
+ - `faraday-retry` (~> 2.0) - Retry middleware
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/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ @AGENTS.md
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.1'
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.1
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-02-04 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
@@ -45,11 +45,17 @@ executables: []
45
45
  extensions: []
46
46
  extra_rdoc_files: []
47
47
  files:
48
+ - ".github/actionlint-matcher.json"
49
+ - ".github/dependabot.yml"
48
50
  - ".github/workflows/ci.yml"
51
+ - ".github/workflows/github-actions-scan.yml"
49
52
  - ".github/workflows/release.yml"
50
53
  - ".mise.local.toml.example"
51
54
  - ".mise.toml"
52
55
  - ".rubocop.yml"
56
+ - AGENTS.md
57
+ - CHANGELOG.md
58
+ - CLAUDE.md
53
59
  - README.md
54
60
  - Rakefile
55
61
  - lib/data_nexus.rb
@@ -61,6 +67,7 @@ files:
61
67
  - lib/data_nexus/resources/member_consents.rb
62
68
  - lib/data_nexus/resources/member_enrollments.rb
63
69
  - lib/data_nexus/resources/members.rb
70
+ - lib/data_nexus/resources/program_list.rb
64
71
  - lib/data_nexus/resources/program_member.rb
65
72
  - lib/data_nexus/resources/program_members.rb
66
73
  - lib/data_nexus/resources/programs.rb
@@ -72,7 +79,7 @@ licenses:
72
79
  metadata:
73
80
  homepage_uri: https://github.com/DartHealth/datanexus-ruby
74
81
  source_code_uri: https://github.com/DartHealth/datanexus-ruby
75
- changelog_uri: https://github.com/DartHealth/datanexus-ruby/releases
82
+ changelog_uri: https://github.com/DartHealth/datanexus-ruby/blob/main/CHANGELOG.md
76
83
  rubygems_mfa_required: 'true'
77
84
  post_install_message:
78
85
  rdoc_options: []