-
Notifications
You must be signed in to change notification settings - Fork 0
v1 prep #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
v1 prep #6
Changes from 18 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
c7d4df6
Initial project setup with TypeScript SDK for OMOPHub API, including …
17138cf
Enhance OMOPHub SDK with new concepts and domains resources, update N…
9f6b495
Refactor concepts and domains methods for improved query handling and…
d819a88
Merge pull request #2 from OMOPHub/initial-version
alex-omophub 6d3eeab
Enhance OMOPHub SDK with new hierarchy, mappings, and relationships r…
5752c73
Refactor mappings and search interfaces for improved validation and r…
4dbfaac
Merge pull request #3 from OMOPHub/search
alex-omophub 82b3dd4
Update biome configuration to include e2e tests in linting and format…
c7dde2b
Enhance environment variable loading in e2e tests with improved parsi…
7972d55
Merge pull request #4 from OMOPHub/fhir
alex-omophub 8f80afe
Release version 1.0.0 of the OMOPHub Node.js SDK. Introduced comprehe…
1422115
Update package.json to include exports field for module resolution an…
fb4031b
Update CI workflow to include 'develop' branch for push and pull requ…
1e570ac
Update CHANGELOG for version 1.0.0 release, enhance FHIR resolver to …
4eed1c8
Merge pull request #5 from OMOPHub/examples
alex-omophub bf1419d
Refactor auth and utility functions for improved error handling and d…
c6ca3fa
Enhance `parseRetryAfter` function to strictly validate HTTP-date for…
4c52b25
Enhance CI workflow by adding manual triggering capability and introd…
9842164
Refine CI workflow to enforce branch restrictions for integration job…
f040834
Implement retry logic for POST requests on 429 status codes by introd…
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| name: CI | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main, develop] | ||
| pull_request: | ||
| branches: [main, develop] | ||
| workflow_dispatch: # Allow manual triggering | ||
|
|
||
| jobs: | ||
| test: | ||
| runs-on: ubuntu-latest | ||
| strategy: | ||
| matrix: | ||
| node-version: [22, 24] | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: ${{ matrix.node-version }} | ||
| cache: npm | ||
|
|
||
| - run: npm ci | ||
| - run: npm run build | ||
| - run: npm run lint | ||
| - run: npm run typecheck | ||
| - run: npm test | ||
|
|
||
| integration: | ||
| # Only run on push to main/develop, not on PRs (avoids leaking the | ||
| # live API key to forked-PR runners and keeps PR CI fast). | ||
| if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: 22 | ||
| cache: npm | ||
|
|
||
| - run: npm ci | ||
| - run: npm run build | ||
| - name: Run e2e tests against live API | ||
| env: | ||
| OMOPHUB_API_KEY: ${{ secrets.TEST_API_KEY }} | ||
| run: npm run test:e2e | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| name: Publish to npm | ||
|
|
||
| on: | ||
| release: | ||
| types: [published] | ||
|
|
||
| jobs: | ||
| publish: | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| id-token: write | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: 22 | ||
| registry-url: https://registry.npmjs.org | ||
| cache: npm | ||
|
|
||
| - run: npm ci | ||
| - run: npm run build | ||
| - run: npm test | ||
| - run: npm publish --provenance --access public | ||
| env: | ||
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| node_modules/ | ||
| dist/ | ||
| coverage/ | ||
| .env | ||
| .env.local | ||
| .env.*.local | ||
| *.log | ||
| npm-debug.log* | ||
| .DS_Store | ||
| .idea/ | ||
| .vscode/ | ||
| *.swp | ||
| *.swo |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| 22 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # Changelog | ||
|
|
||
| All notable changes to this project will be documented in this file. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||
|
|
||
| ## [Unreleased] | ||
|
|
||
| ## [1.0.0] - 2026-05-31 | ||
|
|
||
| ### Added | ||
|
|
||
| - Initial scaffolding: `OMOPHub` client skeleton with API key + base URL resolution, env-var precedence, and constructor-time validation. | ||
| - TypeScript build (ES2022, NodeNext, strict mode). | ||
| - Biome v2 lint + format. | ||
| - Vitest test runner with v8 coverage and a 90% statements threshold. | ||
| - CI workflow on Node 22 and 24 (`build`, `lint`, `typecheck`, `test`). | ||
| - npm publish workflow triggered by GitHub release, with provenance attestation. | ||
| - HTTP layer: `get` / `post` / `patch` / `put` / `delete` methods on `OMOPHub` with retry on 429 + 502/503/504 + network errors. Full-jitter exponential backoff (500ms → 8s), `Retry-After` honoured up to 60s. | ||
| - Discriminated `Response<T> = { data, error, meta, headers }` return type - errors never throw from network paths. | ||
| - 16-code `OMOPHUB_ERROR_CODE_KEY` union with stable codes mapped from HTTP status + server-provided error codes. | ||
| - `OMOPHubError` (thrown only on constructor misuse) and `OMOPHubIteratorError` (thrown from future async iterators). | ||
| - Common request-option interfaces: `PerCallOptions`, `GetOptions`, `PostOptions` (with `idempotencyKey`), `PatchOptions`, `PutOptions`, `DeleteOptions`. | ||
| - Pagination types: `PaginationOptions`, `PaginationMeta`, `PaginatedData<T>`. | ||
| - Vocab-release mixin + utility types (`RequireAtLeastOne`, `RequireExactlyOne`). | ||
| - Query builder: camelCase → snake_case, array → comma-join, null/undefined dropped. | ||
| - Envelope unwrap: tolerates both `{ success, data, meta }` and raw payload bodies. | ||
| - AbortSignal composition: client timeout + caller signal merged via `AbortSignal.any`; caller aborts propagate as thrown `AbortError`, timeouts return `timeout_error`. | ||
| - `X-Vocab-Version` header injection when `vocabVersion` option is set. | ||
| - First resource: `client.vocabularies.list()` with snake_case query serialisation, pagination, and error mapping. | ||
| - Test fixtures (`DIABETES_CONCEPT_ID` etc.) and mock-fetch helpers for `vi.fn`-based testing without external mock libraries. | ||
|
|
||
| ### Added (resources) | ||
|
|
||
| - `client.concepts` - 7 methods: `get`, `getByCode`, `batch`, `suggest`, `related`, `relationships`, `recommended`. `concepts.get(0)` accepts the OMOP unmapped sentinel (R-SDK bug fix). `batch` validates 1–100 IDs synthetically; `recommended` validates `conceptIds` ≤ 100, `relationshipTypes` ≤ 20, `vocabularyIds`/`domainIds` ≤ 50. | ||
| - `client.vocabularies` extended with 6 methods: `get`, `stats`, `domainStats`, `domains` (vocab-scoped), `conceptClasses`, `concepts`. | ||
| - `client.domains` - 2 methods: `list`, `concepts`. | ||
| - `client.search` - 11 methods: `basic`, `basicIter`, `basicAll`, `advanced`, `autocomplete`, `semantic`, `semanticIter`, `semanticAll`, `bulkBasic`, `bulkSemantic`, `similar`. `bulkBasic` validates 1–50 searches; `bulkSemantic` validates 1–25; `similar` enforces XOR of `conceptId`/`conceptName`/`query` both at the TS type level (discriminated union) and at runtime. | ||
| - `client.hierarchy` - 3 methods: `get` (flat or graph format), `ancestors`, `descendants`. Server caps `maxLevels` at 20. | ||
| - `client.relationships` - 2 methods: `get` (shares wire endpoint with `concepts.relationships` - kept as parallel discoverable surface), `types`. | ||
| - `client.mappings` - 2 methods: `get` and `map`. `map` enforces XOR of `sourceConcepts` vs `sourceCodes` at both type and runtime levels. `vocabRelease` is routed to the `?vocab_release=` query parameter rather than the JSON body (matches Python SDK convention). JSDoc documents the Procedure-domain vocabulary priority chain (SNOMED → LOINC → CPT4 → HCPCS → ICD10PCS → ICD9Proc → OPCS4 → OMOP Extension). | ||
| - `ConceptHierarchyNode` extended with `domain_id`, `concept_class_id`, `standard_concept` optional fields - now matches Python's `HierarchyConcept` and is re-exported as `HierarchyConcept`/`Ancestor`/`Descendant` from the hierarchy module. | ||
| - `ConceptRelationship` re-exported as `Relationship` from the relationships module - kept in sync via type alias. | ||
| - `client.fhir` - 3 methods: `resolve` (accepts both flat `{ system, code }` and nested `{ coding: {...} }` forms, mirroring the Python SDK's `_extract_coding`), `resolveBatch` (1–100 codings), `resolveCodeableConcept` (1–20 codings). All three validate synthetically before issuing requests. | ||
| - Standalone helpers (no client required): | ||
| - `omophubFhirUrl(version)` - returns the URL of OMOPHub's hosted FHIR Terminology Service (`'r4'` default, also `'r4b'`, `'r5'`, `'r6'`). | ||
| - `getApiKey()`, `setApiKey(key)`, `hasApiKey()` - env-backed helpers reading `OMOPHUB_API_KEY` from `process.env`. `setApiKey` throws `OMOPHubError` on edge runtimes that lack a writable `process.env`. | ||
| - FHIR `Coding` type uses camelCase (`userSelected`, `vocabularyId`) to match the FHIR JSON spec - converted to snake_case at the wire via `toSnakeCaseKeys`. | ||
| - README polish: install + config table + per-resource usage examples for all 8 resources + error-handling guide + async-iterator guide + Python/R migration table. | ||
|
|
||
| ### SDK surface at v1.0.0 - 37 methods across 8 resources | ||
|
|
||
| ``` | ||
| client.concepts - get, getByCode, batch, suggest, related, relationships, recommended (7) | ||
| client.search - basic, basicIter, basicAll, advanced, autocomplete, semantic, | ||
| semanticIter, semanticAll, bulkBasic, bulkSemantic, similar (11) | ||
| client.vocabularies - list, get, stats, domainStats, domains, conceptClasses, concepts (7) | ||
| client.domains - list, concepts (2) | ||
| client.hierarchy - get, ancestors, descendants (3) | ||
| client.relationships - get, types (2) | ||
| client.mappings - get, map (2) | ||
| client.fhir - resolve, resolveBatch, resolveCodeableConcept (3) | ||
| Σ = 37 | ||
|
|
||
| Standalone: omophubFhirUrl, getApiKey, setApiKey, hasApiKey | ||
| ``` | ||
|
|
||
| <!-- Reference-style version links. Update when the v1.0.0 tag is cut. --> | ||
| [Unreleased]: https://github.com/OMOPHub/omophub-node/compare/v1.0.0...HEAD | ||
| [1.0.0]: https://github.com/OMOPHub/omophub-node/releases/tag/v1.0.0 | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,230 @@ | ||
| # Contributing to OMOPHub Node.js SDK | ||
|
|
||
| First off, thank you for considering contributing to OMOPHub! | ||
|
|
||
| ## How Can I Contribute? | ||
|
|
||
| ### Reporting Bugs | ||
|
|
||
| Before creating bug reports, please check the [existing issues](https://github.com/OMOPHub/omophub-node/issues) to avoid duplicates. | ||
|
|
||
| When creating a bug report, please include: | ||
|
|
||
| - **Node version** (`node --version`) | ||
| - **Package manager + version** (`npm --version` / `pnpm --version` / `bun --version`) | ||
| - **SDK version** (`npm ls @omophub/omophub-node`) | ||
| - **Runtime** (Node, Deno, Bun, Cloudflare Workers, Vercel Edge, browser) | ||
| - **Operating system** | ||
| - **Minimal code example** that reproduces the issue | ||
| - **Full error output** - `error.name`, `error.statusCode`, `error.message`, and `error.requestId` if available | ||
| - **Expected vs actual behavior** | ||
|
|
||
| If you're seeing a transport-level failure, include the response headers - especially `x-request-id` - so we can correlate against server logs. | ||
|
|
||
| ### Suggesting Features | ||
|
|
||
| Feature requests are welcome! Please open an issue with: | ||
|
|
||
| - Clear description of the feature | ||
| - Use case: why would this be useful? | ||
| - Proposed TypeScript surface (option keys, return shape) | ||
| - Possible implementation approach (optional) | ||
|
|
||
| ### Pull Requests | ||
|
|
||
| 1. **Fork the repository** and create your branch from `main` | ||
| 2. **Install dependencies:** | ||
| ```bash | ||
| git clone https://github.com/YOUR_USERNAME/omophub-node.git | ||
| cd omophub-node | ||
| npm install | ||
| ``` | ||
| 3. **Make your changes** with clear, descriptive commits | ||
| 4. **Add tests** for new functionality - both unit tests (`test/`) and, when the change touches the wire, integration tests (`e2e/`) | ||
| 5. **Run the full validation suite:** | ||
| ```bash | ||
| npm run typecheck | ||
| npm run lint | ||
| npm test | ||
| npm run build | ||
| ``` | ||
| 6. **Run integration tests** if your change affects request/response shapes: | ||
| ```bash | ||
| OMOPHUB_API_KEY=oh_xxx npm run test:e2e | ||
| ``` | ||
| 7. **Update documentation** - `README.md`, `CHANGELOG.md` under `## [Unreleased]`, and any relevant JSDoc | ||
| 8. **Submit a pull request** with a clear description | ||
|
|
||
| ## Development Setup | ||
|
|
||
| ### Prerequisites | ||
|
|
||
| - **Node.js 22+** (see `.nvmrc`) | ||
| - **npm 10+** (ships with Node 22) | ||
|
|
||
| ### Installation | ||
|
|
||
| ```bash | ||
| # Clone your fork | ||
| git clone https://github.com/YOUR_USERNAME/omophub-node.git | ||
| cd omophub-node | ||
|
|
||
| # Use the pinned Node version | ||
| nvm use # or `fnm use` | ||
|
|
||
| # Install dependencies | ||
| npm install | ||
| ``` | ||
|
|
||
| For integration tests against the live API, create a `.env` file at the repo root: | ||
|
|
||
| ```bash | ||
| echo "export OMOPHUB_API_KEY=oh_xxx" > .env | ||
| ``` | ||
|
|
||
| `.env` is gitignored - never commit your key. | ||
|
|
||
| ### Running Tests | ||
|
|
||
| ```bash | ||
| # Unit tests (fast, fully mocked - no network) | ||
| npm test | ||
|
|
||
| # Watch mode for active development | ||
| npm run test:watch | ||
|
|
||
| # With coverage report | ||
| npm run test:coverage | ||
|
|
||
| # Integration tests against the live API (requires OMOPHUB_API_KEY) | ||
| set -a && source .env && set +a | ||
| npm run test:e2e | ||
|
|
||
| # Run a single test file | ||
| npx vitest run test/concepts/concepts.test.ts | ||
|
|
||
| # Run tests matching a pattern | ||
| npx vitest run -t "bulk" | ||
| ``` | ||
|
|
||
| Unit tests live in `test/` and use `vitest` with `vi.fn()`-based mock fetches - no external mock libraries. Integration tests live in `e2e/` and exercise the SDK against `api.omophub.com/v1`; they tolerate rate limits and known slow paths via the helpers in `e2e/_helpers.ts`. | ||
|
|
||
| ### Code Style | ||
|
|
||
| We use: | ||
|
|
||
| - **Biome v2** for linting and formatting | ||
| - **TypeScript strict mode** (no `any`, no implicit returns, no unused locals) | ||
|
|
||
| ```bash | ||
| # Check lint + format | ||
| npm run lint | ||
|
|
||
| # Auto-fix lint + format | ||
| npm run lint:fix | ||
|
|
||
| # Format only | ||
| npm run format | ||
|
|
||
| # Type-check (src + e2e) | ||
| npm run typecheck | ||
| ``` | ||
|
|
||
| Run `npm run lint:fix && npm run typecheck` before pushing. CI runs the same checks. | ||
|
|
||
| ### Running Examples | ||
|
|
||
| The `examples/` directory contains runnable scripts you can use to smoke-test your changes end-to-end: | ||
|
|
||
| ```bash | ||
| set -a && source .env && set +a | ||
| npx tsx examples/basic-usage.ts | ||
| npx tsx examples/fhir-resolver.ts | ||
| ``` | ||
|
|
||
| If your change affects a public method, add or update an example. | ||
|
|
||
| ## Project Structure | ||
|
|
||
| ``` | ||
| omophub-node/ | ||
| ├── src/ | ||
| │ ├── index.ts # Public API exports | ||
| │ ├── client.ts # OMOPHub client + HTTP dispatch + retry | ||
| │ ├── errors.ts # OMOPHubError, OMOPHubIteratorError, error codes | ||
| │ ├── version.ts # SDK version (auto-bumped) | ||
| │ ├── interfaces.ts # Shared request/response/option types | ||
| │ ├── auth/ # API key env helpers | ||
| │ ├── common/ # syntheticError, pagination, toSnakeCaseKeys, … | ||
| │ ├── concepts/ | ||
| │ │ ├── concepts.ts # Concepts resource methods | ||
| │ │ └── interfaces/ # Per-method option + response types | ||
| │ ├── search/ | ||
| │ ├── hierarchy/ | ||
| │ ├── relationships/ | ||
| │ ├── mappings/ | ||
| │ ├── vocabularies/ | ||
| │ ├── domains/ | ||
| │ └── fhir/ | ||
| ├── test/ # Unit tests (mock-fetch, no network) | ||
| ├── e2e/ # Integration tests (live api.omophub.com) | ||
| ├── examples/ # Runnable example scripts | ||
| ├── package.json | ||
| ├── tsconfig.json # src build config | ||
| ├── tsconfig.e2e.json # e2e typecheck config | ||
| ├── tsconfig.examples.json # examples typecheck config | ||
| ├── biome.json # Lint + format rules | ||
| ├── vitest.config.ts # Unit test runner | ||
| └── vitest.e2e.config.ts # Integration test runner (fileParallelism: false) | ||
| ``` | ||
|
|
||
| ## SDK Conventions | ||
|
|
||
| A few patterns are load-bearing - please follow them when adding new methods: | ||
|
|
||
| - **Discriminated returns, not exceptions.** Every method returns `Promise<{ data, error, meta, headers }>`. The only exceptions the SDK throws are `OMOPHubError` (constructor misuse) and `OMOPHubIteratorError` (`*Iter` async-generator page failures). Network errors, 404s, 429s, validation failures - all returned as `error` values. | ||
| - **camelCase TypeScript surface, snake_case wire.** Request options take camelCase keys (`vocabularyIds`, `pageSize`); they're converted to snake_case at the request boundary via `toSnakeCaseKeys`. Response types use snake_case to match the wire - never client-translated. | ||
| - **Synthetic validation when feasible.** Reject obviously-bad inputs (empty arrays, out-of-range counts, missing required fields) with `syntheticError(...)` before making a network call. Match the wire-shape of a real server error so callers' error-handling code doesn't have to branch. | ||
| - **Positional path args + merged options.** Prefer `client.concepts.get(201826, { includeRelationships: true })` over `({ conceptId: 201826, ... })`. Path params are positional; everything else goes in a single options object. | ||
| - **Idempotency-key retries.** GET/HEAD/OPTIONS/PUT/DELETE retry automatically on transient failures. POST/PATCH only retry when the caller sets `{ idempotencyKey: '...' }` - never silently retry a non-idempotent write. | ||
| - **Pagination metadata on the outer envelope.** `meta.pagination` lives on the response, never inside `data`. Add `*Iter` (async generator) and `*All` (eager collector with error accumulation) variants for any new paginated resource. | ||
|
|
||
| When the live API returns a shape that doesn't match the existing types, **fix the type to match the wire** - don't transform on the response path. Log the discovery in `CHANGELOG.md` so the wire-drift history stays traceable. | ||
|
|
||
| ## Commit Messages | ||
|
|
||
| We follow [Conventional Commits](https://www.conventionalcommits.org/): | ||
|
|
||
| - `feat:` New feature | ||
| - `fix:` Bug fix | ||
| - `docs:` Documentation changes | ||
| - `test:` Adding or updating tests | ||
| - `refactor:` Code refactoring | ||
| - `chore:` Maintenance tasks | ||
|
|
||
| Examples: | ||
| ``` | ||
| feat: add semantic search endpoint | ||
| fix: handle rate limit errors correctly | ||
| docs: update README with new examples | ||
| test: add integration tests for batch concept lookup | ||
| ``` | ||
|
|
||
| ## Release Process | ||
|
|
||
| Releases are cut by maintainers via GitHub Releases - **do not bump the version in `package.json` in your PR**. The publish workflow (`.github/workflows/publish.yml`) runs on release-published, publishes to npm with provenance attestation, and tags the registry version. | ||
|
|
||
| For each PR, add a bullet under `## [Unreleased]` in `CHANGELOG.md` describing the user-visible change. | ||
|
|
||
| ## Questions? | ||
|
|
||
| - Open a [GitHub Discussion](https://github.com/OMOPHub/omophub-node/discussions) | ||
| - Email: support@omophub.com | ||
|
|
||
| ## License | ||
|
|
||
| By contributing, you agree that your contributions will be licensed under the MIT License. | ||
|
|
||
| --- | ||
|
|
||
| Thank you for helping make OMOPHub better! |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.