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 +4 -4
- data/.github/actionlint-matcher.json +17 -0
- data/.github/dependabot.yml +21 -0
- data/.github/workflows/ci.yml +10 -4
- data/.github/workflows/github-actions-scan.yml +80 -0
- data/.github/workflows/release.yml +40 -6
- data/AGENTS.md +62 -0
- data/CHANGELOG.md +69 -0
- data/CLAUDE.md +1 -0
- data/README.md +55 -17
- data/lib/data_nexus/client.rb +21 -6
- data/lib/data_nexus/collection.rb +25 -5
- data/lib/data_nexus/resources/program_list.rb +68 -0
- data/lib/data_nexus/resources/program_members.rb +4 -5
- data/lib/data_nexus/version.rb +1 -1
- metadata +10 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 11502860850ccde0aa9b28ec2770d583d716af03d2d8a2cba9beb60383933dae
|
|
4
|
+
data.tar.gz: a9e2a9215fb714a6a015d629de69dc112d55697f8d1c227a61689b2f8fbe3c56
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -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@
|
|
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@
|
|
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@
|
|
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
|
-
|
|
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 $
|
|
76
|
+
run: gem push "$GEM_FILE"
|
|
43
77
|
env:
|
|
44
|
-
|
|
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:
|
|
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
|
data/lib/data_nexus/client.rb
CHANGED
|
@@ -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
|
|
67
|
+
# Access programs
|
|
64
68
|
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
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
|
-
# @
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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]
|
|
44
|
+
# @return [Collection] The first page of matching members
|
|
46
45
|
#
|
|
47
46
|
# @example Basic listing
|
|
48
47
|
# collection = client.programs("uuid").members.list
|
data/lib/data_nexus/version.rb
CHANGED
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.
|
|
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
|
|
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/
|
|
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: []
|