diff --git a/.github/workflows/build_docker.yml b/.github/workflows/build_docker.yml
index 270a358d4..c07d927e7 100644
--- a/.github/workflows/build_docker.yml
+++ b/.github/workflows/build_docker.yml
@@ -70,7 +70,7 @@ jobs:
IMAGE_TAG: ${{ steps.get-image-tag.outputs.image_tag }}
run: |
docker build -t $REGISTRY/$REGISTRY_ALIAS/$REPOSITORY:$IMAGE_TAG .
- docker build --build-arg NEPTUNE_NOTEBOOK=true -t $REGISTRY/$REGISTRY_ALIAS/$REPOSITORY:sagemaker-$IMAGE_TAG .
+ docker tag $REGISTRY/$REGISTRY_ALIAS/$REPOSITORY:$IMAGE_TAG $REGISTRY/$REGISTRY_ALIAS/$REPOSITORY:sagemaker-$IMAGE_TAG
docker push $REGISTRY/$REGISTRY_ALIAS/$REPOSITORY:$IMAGE_TAG
docker push $REGISTRY/$REGISTRY_ALIAS/$REPOSITORY:sagemaker-$IMAGE_TAG
diff --git a/.github/workflows/test_build_docker.yml b/.github/workflows/test_build_docker.yml
index a41d37674..bc95bada2 100644
--- a/.github/workflows/test_build_docker.yml
+++ b/.github/workflows/test_build_docker.yml
@@ -24,7 +24,6 @@ jobs:
- name: Build Docker image
run: |
docker build -t test-image .
- docker build -t test-image-neptune --build-arg NEPTUNE_NOTEBOOK=true .
- name: Scan Docker image for vulnerabilities
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
@@ -36,7 +35,6 @@ jobs:
- name: Ensure openSSL is installed
run: |
docker run --rm --entrypoint="" test-image openssl --version
- docker run --rm --entrypoint="" test-image-neptune openssl --version
- name: Verify unnecessary packages are removed
run: |
@@ -48,14 +46,6 @@ jobs:
(command -v yum && echo "FAIL: yum found" && exit 1) || echo "✓ yum removed"
(command -v dnf && echo "FAIL: dnf found" && exit 1) || echo "✓ dnf removed"
'
- docker run --rm --entrypoint="" test-image-neptune sh -c '
- (command -v npm && echo "FAIL: npm found" && exit 1) || echo "✓ npm removed"
- (command -v pnpm && echo "FAIL: pnpm found" && exit 1) || echo "✓ pnpm removed"
- (command -v corepack && echo "FAIL: corepack found" && exit 1) || echo "✓ corepack removed"
- (command -v python3 && echo "FAIL: python3 found" && exit 1) || echo "✓ python3 removed"
- (command -v yum && echo "FAIL: yum found" && exit 1) || echo "✓ yum removed"
- (command -v dnf && echo "FAIL: dnf found" && exit 1) || echo "✓ dnf removed"
- '
- name: Verify server starts and responds
run: |
diff --git a/Dockerfile b/Dockerfile
index 3a0c0546f..1932a5b19 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -20,32 +20,9 @@ RUN yum update -y --releasever 2023.11.20260427 && \
rm -rf /var/cache/yum /var/cache/dnf
FROM base
-ARG NEPTUNE_NOTEBOOK
-ENV NEPTUNE_NOTEBOOK=$NEPTUNE_NOTEBOOK
ENV HOME=/graph-explorer
-# Conditionally set the following environment values using +/- variable expansion
-# https://docs.docker.com/reference/dockerfile/#environment-replacement
-#
-# If NEPTUNE_NOTEBOOK value is set then
-# - GRAPH_EXP_ENV_ROOT_FOLDER = /proxy/9250/explorer
-# - PROXY_SERVER_HTTP_PORT = 9250
-# - LOG_STYLE = cloudwatch
-# Else the values are the defaults
-# - GRAPH_EXP_ENV_ROOT_FOLDER = /explorer
-# - PROXY_SERVER_HTTP_PORT = 80
-# - LOG_STYLE = default
-
-ENV GRAPH_EXP_ENV_ROOT_FOLDER=${NEPTUNE_NOTEBOOK:+/proxy/9250/explorer}
-ENV GRAPH_EXP_ENV_ROOT_FOLDER=${GRAPH_EXP_ENV_ROOT_FOLDER:-/explorer}
-
-ENV PROXY_SERVER_HTTP_PORT=${NEPTUNE_NOTEBOOK:+9250}
-ENV PROXY_SERVER_HTTP_PORT=${PROXY_SERVER_HTTP_PORT:-80}
-
-ENV LOG_STYLE=${NEPTUNE_NOTEBOOK:+cloudwatch}
-ENV LOG_STYLE=${LOG_STYLE:-default}
-
WORKDIR /
COPY . /graph-explorer/
WORKDIR /graph-explorer
diff --git a/docs/adr/0001-unify-docker-image-remove-sagemaker-variant.md b/docs/adr/0001-unify-docker-image-remove-sagemaker-variant.md
new file mode 100644
index 000000000..77918cec7
--- /dev/null
+++ b/docs/adr/0001-unify-docker-image-remove-sagemaker-variant.md
@@ -0,0 +1,143 @@
+# ADR 0001: Unify Docker Image by Removing SageMaker Variant
+
+## Status
+
+Accepted
+
+## Context
+
+Graph Explorer ships two Docker images from the same Dockerfile: a main image and a
+SageMaker image built with `--build-arg NEPTUNE_NOTEBOOK=true`. The build argument
+bakes different environment variable defaults into the image (port 9250 instead of 80,
+cloudwatch logging, a different Vite `base` path for static assets). The SageMaker
+lifecycle script already passes all these values as runtime environment variables,
+making the build-time split unnecessary.
+
+Additionally, the client requires users to manually specify a "Public or Proxy
+Endpoint" URL even though the proxy server and UI are always served from the same
+origin. This creates confusion and an extra configuration step that can be derived
+automatically.
+
+## Decision
+
+Eliminate the separate SageMaker image by:
+
+1. **Using relative asset paths** — set Vite `base: "./"` and add ``
+ to `index.html`. This makes the compiled frontend work behind any reverse proxy
+ without build-time knowledge of the path prefix.
+
+2. **Using relative fetch paths** — the client fetches API routes (sparql, gremlin,
+ openCypher, defaultConnection, etc.) via `new URL("../sparql", document.baseURI)`
+ instead of constructing absolute URLs from a configured proxy endpoint. The server
+ mounts static files at `/explorer` and API routes at `/`, so `../` from the static
+ directory always resolves to the API root.
+
+3. **Always routing through the proxy** — remove the `proxyConnection` toggle and
+ `url` (proxy endpoint) from the connection model. The client always sends requests
+ to the same-origin proxy server. The connection config simplifies to: database
+ endpoint, query engine, and optional IAM settings.
+
+4. **Moving SageMaker defaults to runtime** — `process-environment.sh` reads
+ `NEPTUNE_NOTEBOOK=true` at container startup and writes port/log-style/SSL
+ settings to `.env`. The Dockerfile no longer sets these, allowing the app's
+ built-in defaults (port 80, default log style) to apply when the variable is absent.
+
+5. **Publishing dual tags during transition** — CI builds one image and publishes it
+ under both the regular tag and the `sagemaker-*` tag, so existing lifecycle scripts
+ continue working without modification.
+
+## Consequences
+
+### Positive
+
+- One image to build, test, scan, and publish — halves CI time for Docker.
+- Users no longer need to figure out or configure the proxy server URL.
+- The connection form simplifies to just the database endpoint and auth settings.
+- Deployments behind arbitrary reverse proxies (not just Jupyter) work without
+ build-time configuration.
+- Removes ~20 lines of conditional Dockerfile logic and the two-path defaultConnection
+ fallback hack in the client.
+
+### Negative
+
+- Relative paths create a fixed contract: the API root is always one directory above
+ the static files mount. This is enforced in one place (`server-config.ts`) so drift
+ is unlikely but possible.
+- Legacy stored connections (localStorage) need a read-time migration:
+ `graphDbUrl = old.proxyConnection ? old.graphDbUrl : old.url`.
+- The `sagemaker-*` tags must be published for several release cycles until existing
+ deployed lifecycle scripts are updated.
+- The proxy server must have network access to the target database. Deployments in
+ restricted networks (e.g., private subnets without a NAT gateway) cannot reach
+ databases outside that network — even if the user's browser previously could via
+ direct connections. Users in this scenario need to add network routing.
+
+### Neutral
+
+- `NEPTUNE_NOTEBOOK` remains as a runtime convenience preset (sets port, log style,
+ disables SSL). It is not written to `.env` itself — only its side effects are
+ applied.
+- Extra environment variables passed by old deployments (`PUBLIC_OR_PROXY_ENDPOINT`,
+ `USING_PROXY_SERVER`) are silently ignored — no errors.
+
+## Changes Required
+
+### Dockerfile
+
+- Remove `ARG NEPTUNE_NOTEBOOK` and all conditional ENV logic
+- Remove `GRAPH_EXP_ENV_ROOT_FOLDER`
+- Remove `ENV PROXY_SERVER_HTTP_PORT` and `ENV LOG_STYLE` (defaults from `env.ts`)
+- Keep `EXPOSE 80`, `EXPOSE 443`, `EXPOSE 9250`
+
+### Frontend Build
+
+- `vite.config.ts`: `base: "./"`
+- `index.html`: add ``
+
+### Client Code
+
+- Explorers use `new URL("../sparql", document.baseURI)` etc.
+- `defaultConnection.ts`: single relative fetch, remove SageMaker fallback
+- `RELOAD_URL` becomes `"."`
+- Remove `proxyConnection` and `url` from `ConnectionConfig`
+- `graphDbUrl` is the canonical database URL field
+- `fetchDatabaseRequest.ts`: always send `graph-db-connection-url` header
+- Read-time normalization for legacy data
+
+### Client UI
+
+- Connection form: remove proxy endpoint and proxy toggle, always show "Graph
+ Connection URL" field and IAM toggle
+- Connection display components: simplify to `graphDbUrl`
+
+### Server
+
+- `process-environment.sh`:
+ - Remove `PUBLIC_OR_PROXY_ENDPOINT` / `USING_PROXY_SERVER` handling
+ - When `NEPTUNE_NOTEBOOK=true`: write `PROXY_SERVER_HTTP_PORT=9250` and
+ `LOG_STYLE=cloudwatch` to `.env` (respecting explicit overrides), force SSL off
+ - Stop writing `NEPTUNE_NOTEBOOK` to `.env`
+
+### CI
+
+- Build one image, publish under regular + `sagemaker-*` tags
+- Remove `--build-arg NEPTUNE_NOTEBOOK=true` step
+
+### Lifecycle Script
+
+Existing lifecycle scripts continue working unchanged — they pull `sagemaker-*` tags
+(now an alias for the regular image) and pass `PUBLIC_OR_PROXY_ENDPOINT` /
+`USING_PROXY_SERVER` env vars which the unified image silently ignores.
+
+The bundled example script in this repo is updated to:
+- Pull regular tag instead of `sagemaker-*`
+- Remove `PUBLIC_OR_PROXY_ENDPOINT` and `USING_PROXY_SERVER` from `docker run`
+
+### Documentation
+
+- `docs/references/configuration.md`
+- `docs/guides/deploy-to-ecs-fargate.md`
+- `docs/guides/deploy-to-sagemaker.md`
+- `docs/guides/deploy-with-docker.md`
+- `docs/features/connections.md`
+- `docs/architecture.md`
diff --git a/docs/architecture.md b/docs/architecture.md
index e107ea4ec..a82f8c873 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -13,15 +13,16 @@ Graph Explorer is a client-heavy web application with a thin backend proxy. The
graph LR
Browser["Browser\n(React)"] -- HTTP --> Proxy["Proxy Server\n(Express)"]
Proxy -- HTTP --> DB["Graph Database\n(Neptune, etc.)"]
- Browser -. direct .-> DB
Browser -- persistence --> IDB["IndexedDB\n(localforage)"]
```
-The React client constructs queries and sends them through the proxy server, which forwards requests to the graph database. When connecting to Amazon Neptune, the proxy signs requests with AWS SigV4 credentials. For non-Neptune databases, the proxy is optional — the client can connect directly to a publicly accessible endpoint (shown as the dotted line above).
+The React client constructs queries and sends them through the proxy server using relative URLs, which forwards requests to the graph database. When connecting to Amazon Neptune, the proxy signs requests with AWS SigV4 credentials.
The proxy does not store any user data — all preferences, connections, and query history live in the browser's IndexedDB.
-This split exists because browsers cannot perform SigV4 signing directly (it requires AWS credentials that should not be exposed to the client), and because the proxy can run inside a VPC alongside the database while the browser runs outside it.
+This architecture allows the app to work behind any reverse proxy (SageMaker, custom paths) without build-time configuration, since the client resolves API endpoints relative to its own location. The proxy can run inside a VPC alongside the database while the browser runs outside it.
+
+Because all requests flow through the proxy, the server must have network access to the target database. If the server is in a restricted network (e.g., a private subnet with no NAT gateway), it will not be able to reach databases outside that network even if the user's browser could reach them directly.
## Monorepo Structure
diff --git a/docs/features/connections.md b/docs/features/connections.md
index 5c891db5b..ba4bc310b 100644
--- a/docs/features/connections.md
+++ b/docs/features/connections.md
@@ -10,14 +10,11 @@ For guides on connecting to specific databases, see [Connecting to databases](..
- **Name:** Enter a name for your connection (e.g., `MyNeptuneCluster`).
- **Query Language:** Choose a query language that corresponds to your graph database.
-- **Public or proxy endpoint:** Provide the publicly accessible endpoint URL for a graph database, e.g., Gremlin Server. If connecting to Amazon Neptune, then provide a proxy endpoint URL that is accessible from outside the VPC, e.g., EC2.
- - **Note:** For connecting to Amazon Neptune, ensure that the graph connection URL is in the format `https://[NEPTUNE_ENDPOINT]:8182`, and that the proxy endpoint URL is either `https://[EC2_PUBLIC_HOSTNAME]:443` or `http://[EC2_PUBLIC_HOSTNAME]:80`, depending on the protocol used. Ensure that you don't end either of the URLs with `/`.
-- **Using proxy server:** Check this box if using a proxy endpoint.
-- **Graph connection URL:** Provide the endpoint for the graph database
-- **AWS IAM Auth Enabled:** Check this box if connecting to Amazon Neptune using IAM Auth and SigV4 signed requests
-- **Service Type:** Choose the service type
-- **AWS Region:** Specify the AWS region where the Neptune cluster is hosted (e.g., us-east-1)
-- **Fetch Timeout:** Specify the timeout for the fetch request
+- **Graph Connection URL:** Provide the endpoint URL for your graph database (e.g., `https://[NEPTUNE_ENDPOINT]:8182`). Ensure that the URL does not end with `/`.
+- **AWS IAM Auth Enabled:** Check this box if connecting to Amazon Neptune using IAM Auth and SigV4 signed requests.
+- **Service Type:** Choose the service type (`neptune-db` or `neptune-graph`).
+- **AWS Region:** Specify the AWS region where the Neptune cluster is hosted (e.g., us-east-1).
+- **Fetch Timeout:** Specify the timeout for the fetch request.
- **Neighbor Expansion Limit:** Specify the default limit for neighbor expansion. This will override the app setting for neighbor expansion.
## Available Connections
diff --git a/docs/guides/deploy-to-ecs-fargate.md b/docs/guides/deploy-to-ecs-fargate.md
index 6d8158583..5cc8e2e72 100644
--- a/docs/guides/deploy-to-ecs-fargate.md
+++ b/docs/guides/deploy-to-ecs-fargate.md
@@ -107,14 +107,6 @@ After the request is processed, the console will return you to your certificate
"name": "IAM",
"value": "false"
},
- {
- "name": "USING_PROXY_SERVER",
- "value": "true"
- },
- {
- "name": "PUBLIC_OR_PROXY_ENDPOINT",
- "value": "https://{FQDN_from_step3}"
- },
{
"name": "HOST",
"value": "localhost"
@@ -164,7 +156,6 @@ After the request is processed, the console will return you to your certificate
- `GRAPH_TYPE`: The query language for your initial connection.
- `IAM`: Set this to `true` to use SigV4 signed requests, if your Neptune cluster has IAM db authentication enabled.
- `GRAPH_CONNECTION_URL`: Set this as `https://{NEPTUNE_ENDPOINT}:8182`.
- - `PUBLIC_OR_PROXY_ENDPOINT`: Set this as `https://{Domain name set in Step 5 of "Request an ACM Public Certificate"}`.
- `SERVICE_TYPE`: Set this as `neptune-db` for Neptune database or `neptune-graph` for Neptune Analytics.
6. Click **Create**.
diff --git a/docs/guides/deploy-to-sagemaker.md b/docs/guides/deploy-to-sagemaker.md
index 7898df176..96acff624 100644
--- a/docs/guides/deploy-to-sagemaker.md
+++ b/docs/guides/deploy-to-sagemaker.md
@@ -14,6 +14,12 @@ When the notebook has been started and is in "Ready" state, you can access Graph
https://graph-explorer-notebook-name.notebook.us-west-2.sagemaker.aws/proxy/9250/explorer/
```
+## Network Requirements
+
+All database requests from Graph Explorer are routed through the proxy server running on the SageMaker notebook instance. This means the instance must have network access to any database you want to explore.
+
+If your notebook instance is in a private subnet without a NAT gateway or internet gateway, it will not be able to reach databases outside the VPC. To connect to external databases, ensure the instance has the appropriate network routing (VPC peering, NAT gateway, transit gateway, etc.).
+
## Minimum Database Permissions
By default, the permission policy for the IAM role of the SageMaker instance will have full access to the Neptune Database or Neptune Analytics instance. This means queries executed within Graph Explorer could contain mutations.
diff --git a/docs/guides/deploy-to-sagemaker/install-graph-explorer-lc.sh b/docs/guides/deploy-to-sagemaker/install-graph-explorer-lc.sh
index 06fa3b31b..efd9b4563 100644
--- a/docs/guides/deploy-to-sagemaker/install-graph-explorer-lc.sh
+++ b/docs/guides/deploy-to-sagemaker/install-graph-explorer-lc.sh
@@ -130,17 +130,14 @@ LATEST_ECR_RELEASE=$(curl -k -H "Authorization: Bearer $ECR_TOKEN" https://publi
echo "Pulling and starting graph-explorer..."
if [[ ${EXPLORER_VERSION} == "" ]]; then
- EXPLORER_ECR_TAG=sagemaker-${LATEST_ECR_RELEASE}
+ EXPLORER_ECR_TAG=${LATEST_ECR_RELEASE}
else
- if [[ ${EXPLORER_VERSION//./} -ge 140 ]]; then
- EXPLORER_ECR_TAG=sagemaker-${EXPLORER_VERSION}
- elif [[ ${EXPLORER_VERSION} == *latest* ]]; then
- EXPLORER_ECR_TAG=sagemaker-latest-SNAPSHOT
+ if [[ ${EXPLORER_VERSION} == *latest* ]]; then
+ EXPLORER_ECR_TAG=latest-SNAPSHOT
elif [[ ${EXPLORER_VERSION} == *dev* ]]; then
- EXPLORER_ECR_TAG=sagemaker-dev
+ EXPLORER_ECR_TAG=dev
else
- echo "Specified Graph Explorer version does not support use on SageMaker. Defaulting to latest release."
- EXPLORER_ECR_TAG=sagemaker-${LATEST_ECR_RELEASE}
+ EXPLORER_ECR_TAG=${EXPLORER_VERSION}
fi
fi
echo "Using explorer image tag: ${EXPLORER_ECR_TAG}"
@@ -155,13 +152,10 @@ start_graph_explorer_with_cw_logs() {
--log-opt awslogs-multiline-pattern='^(INFO|DEBUG|ERROR|WARN|TRACE|FATAL)' \
--env LOG_LEVEL=debug \
--env HOST=127.0.0.1 \
- --env PUBLIC_OR_PROXY_ENDPOINT=${EXPLORER_URI} \
--env GRAPH_CONNECTION_URL=${NEPTUNE_URI} \
- --env USING_PROXY_SERVER=true \
--env IAM=${IAM} \
--env AWS_REGION=${AWS_REGION} \
--env SERVICE_TYPE=${SERVICE} \
- --env PROXY_SERVER_HTTPS_CONNECTION=false \
--env NEPTUNE_NOTEBOOK=true public.ecr.aws/neptune/graph-explorer:${EXPLORER_ECR_TAG}
}
@@ -170,13 +164,10 @@ start_graph_explorer_with_default_logs() {
--restart always \
--env LOG_LEVEL=debug \
--env HOST=127.0.0.1 \
- --env PUBLIC_OR_PROXY_ENDPOINT=${EXPLORER_URI} \
--env GRAPH_CONNECTION_URL=${NEPTUNE_URI} \
- --env USING_PROXY_SERVER=true \
--env IAM=${IAM} \
--env AWS_REGION=${AWS_REGION} \
--env SERVICE_TYPE=${SERVICE} \
- --env PROXY_SERVER_HTTPS_CONNECTION=false \
--env NEPTUNE_NOTEBOOK=true public.ecr.aws/neptune/graph-explorer:${EXPLORER_ECR_TAG}
}
diff --git a/docs/guides/deploy-with-docker.md b/docs/guides/deploy-with-docker.md
index cee4c4d24..c8116555a 100644
--- a/docs/guides/deploy-with-docker.md
+++ b/docs/guides/deploy-with-docker.md
@@ -9,7 +9,7 @@ You can find the latest version of the image on
> [!NOTE]
>
-> Make sure to use the version of the image that does not include `sagemaker` in the tag.
+> Both the regular and `sagemaker-*` image tags contain the same image. You can use either.
## Prerequisites
diff --git a/docs/references/configuration.md b/docs/references/configuration.md
index c4b8ef1f8..a90143e81 100644
--- a/docs/references/configuration.md
+++ b/docs/references/configuration.md
@@ -8,16 +8,6 @@ All environment variables for configuring Graph Explorer, organized by concern.
These variables control server behavior, networking, and security.
-### `GRAPH_EXP_ENV_ROOT_FOLDER`
-
-Base path used to serve the `graph-explorer` front end application.
-
-Example: `/explorer`
-
-- Optional
-- Default: `/`
-- Type: `string`
-
### `HOST`
The public hostname of the server. This is used to generate the self-signed SSL certificate at container startup.
@@ -88,6 +78,14 @@ Override path for the folder containing `.env` and `defaultConnection.json`. Whe
- Default: `` (`packages/graph-explorer`)
- Type: `string`
+### `NEPTUNE_NOTEBOOK`
+
+Runtime convenience preset for SageMaker/Jupyter deployments. When set to `true`, configures port 9250, cloudwatch logging, and disables SSL automatically.
+
+- Optional
+- Default: not set
+- Type: `boolean`
+
## Default Connection
To provide a default connection such that initial loads of Graph Explorer always result with the same starting connection, modify the `docker run ...` command to either take in a JSON configuration or runtime environment variables. If you provide both a JSON configuration and environmental variables, the JSON will be prioritized.
@@ -97,19 +95,16 @@ To provide a default connection such that initial loads of Graph Explorer always
These are the valid environment variables used for the default connection, their defaults, and their descriptions.
- Required:
- - `PUBLIC_OR_PROXY_ENDPOINT` - `None`
+ - `GRAPH_CONNECTION_URL` - `None` - The URL of the graph database endpoint.
- Optional
- `GRAPH_TYPE` - `None` - If not specified, multiple connections will be created for every available query language.
- - `USING_PROXY_SERVER` - `False`
- `IAM` - `False`
- `GRAPH_EXP_HTTPS_CONNECTION` - `True` - Controls whether Graph Explorer uses SSL or not
- `PROXY_SERVER_HTTPS_CONNECTION` - `True` - Controls whether the server uses SSL or not
- `GRAPH_EXP_FETCH_REQUEST_TIMEOUT` - `240000` - Controls the timeout for the fetch request. Measured in milliseconds (i.e. 240000 is 240 seconds or 4 minutes).
- `GRAPH_EXP_NODE_EXPANSION_LIMIT` - `None` - Controls the limit for node counts and expansion queries.
- Conditionally Required:
- - Required if `USING_PROXY_SERVER=True`
- - `GRAPH_CONNECTION_URL` - `None`
- - Required if `USING_PROXY_SERVER=True` and `IAM=True`
+ - Required if `IAM=True`
- `AWS_REGION` - `None`
- `SERVICE_TYPE` - `neptune-db`, Set this as `neptune-db` for Neptune database or `neptune-graph` for Neptune Analytics.
@@ -119,9 +114,7 @@ First, create a `config.json` file containing values for the connection attribut
```json
{
- "PUBLIC_OR_PROXY_ENDPOINT": "https://public-endpoint",
"GRAPH_CONNECTION_URL": "https://{your-cluster-id}.us-west-2.neptune.amazonaws.com:8182",
- "USING_PROXY_SERVER": true,
"IAM": true,
"SERVICE_TYPE": "neptune-db",
"AWS_REGION": "us-west-2",
@@ -151,11 +144,9 @@ Provide the desired connection variables directly to the `docker run` command, a
```bash
docker run -p 80:80 -p 443:443 \
--env HOST={hostname-or-ip-address} \
- --env PUBLIC_OR_PROXY_ENDPOINT=https://public-endpoint \
- --env GRAPH_TYPE=gremlin \
- --env USING_PROXY_SERVER=true \
- --env IAM=false \
--env GRAPH_CONNECTION_URL=https://{your-cluster-id}.us-west-2.neptune.amazonaws.com:8182 \
+ --env GRAPH_TYPE=gremlin \
+ --env IAM=true \
--env AWS_REGION=us-west-2 \
--env SERVICE_TYPE=neptune-db \
--env PROXY_SERVER_HTTPS_CONNECTION=true \
diff --git a/packages/graph-explorer-proxy-server/src/process-environment.test.ts b/packages/graph-explorer-proxy-server/src/process-environment.test.ts
index 3fb5552a5..00556b3e6 100644
--- a/packages/graph-explorer-proxy-server/src/process-environment.test.ts
+++ b/packages/graph-explorer-proxy-server/src/process-environment.test.ts
@@ -84,8 +84,8 @@ describe("process-environment.sh", () => {
});
});
- describe("NEPTUNE_NOTEBOOK=true forces SSL off", () => {
- it("overrides both HTTPS vars to false", () => {
+ describe("NEPTUNE_NOTEBOOK=true", () => {
+ it("forces both HTTPS vars to false", () => {
const { envFile } = runScript(workDir, {
NEPTUNE_NOTEBOOK: "true",
});
@@ -93,11 +93,41 @@ describe("process-environment.sh", () => {
expect(envFile).toContain("GRAPH_EXP_HTTPS_CONNECTION=false");
});
- it("writes NEPTUNE_NOTEBOOK=true to .env", () => {
+ it("writes PROXY_SERVER_HTTP_PORT=9250 to .env", () => {
const { envFile } = runScript(workDir, {
NEPTUNE_NOTEBOOK: "true",
});
- expect(envFile).toContain("NEPTUNE_NOTEBOOK=true");
+ expect(envFile).toContain("PROXY_SERVER_HTTP_PORT=9250");
+ });
+
+ it("writes LOG_STYLE=cloudwatch to .env", () => {
+ const { envFile } = runScript(workDir, {
+ NEPTUNE_NOTEBOOK: "true",
+ });
+ expect(envFile).toContain("LOG_STYLE=cloudwatch");
+ });
+
+ it("does not write NEPTUNE_NOTEBOOK to .env", () => {
+ const { envFile } = runScript(workDir, {
+ NEPTUNE_NOTEBOOK: "true",
+ });
+ expect(envFile).not.toContain("NEPTUNE_NOTEBOOK=");
+ });
+
+ it("respects explicit PROXY_SERVER_HTTP_PORT override", () => {
+ const { envFile } = runScript(workDir, {
+ NEPTUNE_NOTEBOOK: "true",
+ PROXY_SERVER_HTTP_PORT: "8080",
+ });
+ expect(envFile).not.toContain("PROXY_SERVER_HTTP_PORT=9250");
+ });
+
+ it("respects explicit LOG_STYLE override", () => {
+ const { envFile } = runScript(workDir, {
+ NEPTUNE_NOTEBOOK: "true",
+ LOG_STYLE: "json",
+ });
+ expect(envFile).not.toContain("LOG_STYLE=cloudwatch");
});
});
@@ -106,16 +136,16 @@ describe("process-environment.sh", () => {
const { envFile } = runScript(workDir, {
NEPTUNE_NOTEBOOK: "false",
});
- expect(envFile).toContain("NEPTUNE_NOTEBOOK=false");
expect(envFile).toContain("PROXY_SERVER_HTTPS_CONNECTION=true");
expect(envFile).toContain("GRAPH_EXP_HTTPS_CONNECTION=true");
});
- });
- describe("NEPTUNE_NOTEBOOK unset", () => {
- it("defaults NEPTUNE_NOTEBOOK to false in .env", () => {
- const { envFile } = runScript(workDir);
- expect(envFile).toContain("NEPTUNE_NOTEBOOK=false");
+ it("does not write port or log style", () => {
+ const { envFile } = runScript(workDir, {
+ NEPTUNE_NOTEBOOK: "false",
+ });
+ expect(envFile).not.toContain("PROXY_SERVER_HTTP_PORT");
+ expect(envFile).not.toContain("LOG_STYLE");
});
});
@@ -124,9 +154,7 @@ describe("process-environment.sh", () => {
fs.writeFileSync(
path.join(workDir, "config.json"),
JSON.stringify({
- PUBLIC_OR_PROXY_ENDPOINT: "https://my-endpoint:8182",
GRAPH_TYPE: "sparql",
- USING_PROXY_SERVER: true,
IAM: true,
GRAPH_CONNECTION_URL: "https://my-db:8182",
AWS_REGION: "us-west-2",
@@ -140,62 +168,32 @@ describe("process-environment.sh", () => {
expect(envFile).toContain("PROXY_SERVER_HTTPS_CONNECTION=false");
expect(envFile).toContain("GRAPH_EXP_HTTPS_CONNECTION=false");
expect(defaultConnection).toMatchObject({
- GRAPH_EXP_PUBLIC_OR_PROXY_ENDPOINT: "https://my-endpoint:8182",
GRAPH_EXP_GRAPH_TYPE: "sparql",
- GRAPH_EXP_USING_PROXY_SERVER: true,
GRAPH_EXP_IAM: true,
GRAPH_EXP_CONNECTION_URL: "https://my-db:8182",
GRAPH_EXP_AWS_REGION: "us-west-2",
});
});
-
- it("config.json overrides conflicting env vars", () => {
- fs.writeFileSync(
- path.join(workDir, "config.json"),
- JSON.stringify({
- PUBLIC_OR_PROXY_ENDPOINT: "https://from-config:8182",
- GRAPH_TYPE: "sparql",
- }),
- );
-
- const { defaultConnection } = runScript(workDir, {
- PUBLIC_OR_PROXY_ENDPOINT: "https://from-env:8182",
- GRAPH_TYPE: "gremlin",
- });
-
- expect(defaultConnection).toHaveProperty(
- "GRAPH_EXP_PUBLIC_OR_PROXY_ENDPOINT",
- "https://from-config:8182",
- );
- expect(defaultConnection).toHaveProperty(
- "GRAPH_EXP_GRAPH_TYPE",
- "sparql",
- );
- });
});
describe("defaultConnection.json generation", () => {
- it("creates defaultConnection.json with GRAPH_EXP_ prefixed fields", () => {
+ it("creates defaultConnection.json when GRAPH_CONNECTION_URL is set", () => {
const { defaultConnection } = runScript(workDir, {
- PUBLIC_OR_PROXY_ENDPOINT: "https://endpoint:8182",
+ GRAPH_CONNECTION_URL: "https://db:8182",
GRAPH_TYPE: "gremlin",
- USING_PROXY_SERVER: "true",
IAM: "false",
- GRAPH_CONNECTION_URL: "https://db:8182",
AWS_REGION: "eu-west-1",
});
expect(defaultConnection).toMatchObject({
- GRAPH_EXP_PUBLIC_OR_PROXY_ENDPOINT: "https://endpoint:8182",
GRAPH_EXP_GRAPH_TYPE: "gremlin",
- GRAPH_EXP_USING_PROXY_SERVER: true,
GRAPH_EXP_IAM: false,
GRAPH_EXP_CONNECTION_URL: "https://db:8182",
GRAPH_EXP_AWS_REGION: "eu-west-1",
});
});
- it("does not create defaultConnection.json without PUBLIC_OR_PROXY_ENDPOINT", () => {
+ it("does not create defaultConnection.json without GRAPH_CONNECTION_URL", () => {
const { defaultConnection } = runScript(workDir, {
GRAPH_TYPE: "gremlin",
});
@@ -204,7 +202,7 @@ describe("process-environment.sh", () => {
it("defaults SERVICE_TYPE to neptune-db", () => {
const { defaultConnection } = runScript(workDir, {
- PUBLIC_OR_PROXY_ENDPOINT: "https://endpoint:8182",
+ GRAPH_CONNECTION_URL: "https://db:8182",
});
expect(defaultConnection).toHaveProperty(
"GRAPH_EXP_SERVICE_TYPE",
@@ -212,19 +210,9 @@ describe("process-environment.sh", () => {
);
});
- it("defaults USING_PROXY_SERVER to false", () => {
- const { defaultConnection } = runScript(workDir, {
- PUBLIC_OR_PROXY_ENDPOINT: "https://endpoint:8182",
- });
- expect(defaultConnection).toHaveProperty(
- "GRAPH_EXP_USING_PROXY_SERVER",
- false,
- );
- });
-
it("defaults IAM to false", () => {
const { defaultConnection } = runScript(workDir, {
- PUBLIC_OR_PROXY_ENDPOINT: "https://endpoint:8182",
+ GRAPH_CONNECTION_URL: "https://db:8182",
});
expect(defaultConnection).toHaveProperty("GRAPH_EXP_IAM", false);
});
@@ -233,7 +221,7 @@ describe("process-environment.sh", () => {
describe("SERVICE_TYPE=neptune-graph auto-sets openCypher", () => {
it("sets GRAPH_TYPE to openCypher when SERVICE_TYPE is neptune-graph", () => {
const { defaultConnection } = runScript(workDir, {
- PUBLIC_OR_PROXY_ENDPOINT: "https://endpoint:8182",
+ GRAPH_CONNECTION_URL: "https://db:8182",
SERVICE_TYPE: "neptune-graph",
});
expect(defaultConnection).toHaveProperty(
@@ -244,7 +232,7 @@ describe("process-environment.sh", () => {
it("does not set GRAPH_TYPE when SERVICE_TYPE is neptune-db and no GRAPH_TYPE given", () => {
const { defaultConnection } = runScript(workDir, {
- PUBLIC_OR_PROXY_ENDPOINT: "https://endpoint:8182",
+ GRAPH_CONNECTION_URL: "https://db:8182",
SERVICE_TYPE: "neptune-db",
});
expect(defaultConnection).not.toHaveProperty("GRAPH_EXP_GRAPH_TYPE");
@@ -252,7 +240,7 @@ describe("process-environment.sh", () => {
it("explicit GRAPH_TYPE takes priority over neptune-graph auto-detection", () => {
const { defaultConnection } = runScript(workDir, {
- PUBLIC_OR_PROXY_ENDPOINT: "https://endpoint:8182",
+ GRAPH_CONNECTION_URL: "https://db:8182",
SERVICE_TYPE: "neptune-graph",
GRAPH_TYPE: "sparql",
});
@@ -269,16 +257,14 @@ describe("process-environment.sh", () => {
runScript(workDir, {
CONFIGURATION_FOLDER_PATH: customFolder,
- PUBLIC_OR_PROXY_ENDPOINT: "https://endpoint:8182",
+ GRAPH_CONNECTION_URL: "https://db:8182",
});
- // Files exist at the custom path
expect(fs.existsSync(path.join(customFolder, ".env"))).toBe(true);
expect(
fs.existsSync(path.join(customFolder, "defaultConnection.json")),
).toBe(true);
- // Files do not exist at the default path
const defaultFolder = path.join(workDir, "packages", "graph-explorer");
expect(fs.existsSync(path.join(defaultFolder, ".env"))).toBe(false);
expect(
@@ -288,16 +274,16 @@ describe("process-environment.sh", () => {
});
describe("default values for optional fields", () => {
- it("defaults GRAPH_CONNECTION_URL to empty string", () => {
+ it("defaults GRAPH_CONNECTION_URL to empty string in output", () => {
const { defaultConnection } = runScript(workDir, {
- PUBLIC_OR_PROXY_ENDPOINT: "https://endpoint:8182",
+ GRAPH_CONNECTION_URL: "",
});
- expect(defaultConnection).toHaveProperty("GRAPH_EXP_CONNECTION_URL", "");
+ // No defaultConnection generated when GRAPH_CONNECTION_URL is empty
+ expect(defaultConnection).toBeNull();
});
it("preserves path in GRAPH_CONNECTION_URL", () => {
const { defaultConnection } = runScript(workDir, {
- PUBLIC_OR_PROXY_ENDPOINT: "http://localhost:8080",
GRAPH_CONNECTION_URL: "http://blazegraph:9999/blazegraph/namespace/kb",
});
expect(defaultConnection).toHaveProperty(
@@ -306,27 +292,16 @@ describe("process-environment.sh", () => {
);
});
- it("preserves trailing slash in GRAPH_CONNECTION_URL", () => {
- const { defaultConnection } = runScript(workDir, {
- PUBLIC_OR_PROXY_ENDPOINT: "http://localhost:8080",
- GRAPH_CONNECTION_URL: "http://blazegraph:9999/blazegraph/namespace/kb/",
- });
- expect(defaultConnection).toHaveProperty(
- "GRAPH_EXP_CONNECTION_URL",
- "http://blazegraph:9999/blazegraph/namespace/kb/",
- );
- });
-
it("defaults AWS_REGION to empty string", () => {
const { defaultConnection } = runScript(workDir, {
- PUBLIC_OR_PROXY_ENDPOINT: "https://endpoint:8182",
+ GRAPH_CONNECTION_URL: "https://db:8182",
});
expect(defaultConnection).toHaveProperty("GRAPH_EXP_AWS_REGION", "");
});
it("passes through custom SERVICE_TYPE value", () => {
const { defaultConnection } = runScript(workDir, {
- PUBLIC_OR_PROXY_ENDPOINT: "https://endpoint:8182",
+ GRAPH_CONNECTION_URL: "https://db:8182",
SERVICE_TYPE: "neptune-graph",
});
expect(defaultConnection).toHaveProperty(
@@ -357,53 +332,14 @@ describe("process-environment.sh", () => {
);
const { defaultConnection } = runScript(workDir, {
- PUBLIC_OR_PROXY_ENDPOINT: "https://endpoint:8182",
+ GRAPH_CONNECTION_URL: "https://db:8182",
});
expect(defaultConnection).not.toHaveProperty("OLD_KEY");
expect(defaultConnection).toHaveProperty(
- "GRAPH_EXP_PUBLIC_OR_PROXY_ENDPOINT",
- "https://endpoint:8182",
- );
- });
- });
-
- describe("defaultConnection.json has all expected keys", () => {
- it("contains exactly the expected keys when all values provided", () => {
- const { defaultConnection } = runScript(workDir, {
- PUBLIC_OR_PROXY_ENDPOINT: "https://endpoint:8182",
- SERVICE_TYPE: "neptune-db",
- GRAPH_TYPE: "gremlin",
- USING_PROXY_SERVER: "true",
- IAM: "true",
- GRAPH_CONNECTION_URL: "https://db:8182",
- AWS_REGION: "us-east-1",
- });
-
- expect(Object.keys(defaultConnection!).sort()).toEqual([
- "GRAPH_EXP_AWS_REGION",
"GRAPH_EXP_CONNECTION_URL",
- "GRAPH_EXP_GRAPH_TYPE",
- "GRAPH_EXP_IAM",
- "GRAPH_EXP_PUBLIC_OR_PROXY_ENDPOINT",
- "GRAPH_EXP_SERVICE_TYPE",
- "GRAPH_EXP_USING_PROXY_SERVER",
- ]);
- });
-
- it("omits GRAPH_EXP_GRAPH_TYPE when neither GRAPH_TYPE nor neptune-graph", () => {
- const { defaultConnection } = runScript(workDir, {
- PUBLIC_OR_PROXY_ENDPOINT: "https://endpoint:8182",
- });
-
- expect(Object.keys(defaultConnection!).sort()).toEqual([
- "GRAPH_EXP_AWS_REGION",
- "GRAPH_EXP_CONNECTION_URL",
- "GRAPH_EXP_IAM",
- "GRAPH_EXP_PUBLIC_OR_PROXY_ENDPOINT",
- "GRAPH_EXP_SERVICE_TYPE",
- "GRAPH_EXP_USING_PROXY_SERVER",
- ]);
+ "https://db:8182",
+ );
});
});
@@ -413,11 +349,6 @@ describe("process-environment.sh", () => {
expect(envFile).toMatch(/^PROXY_SERVER_HTTPS_CONNECTION=true$/m);
});
- it("does not produce commented-out PROXY_SERVER_HTTPS_CONNECTION", () => {
- const { envFile } = runScript(workDir);
- expect(envFile).not.toContain("# PROXY_SERVER_HTTPS_CONNECTION");
- });
-
it("value has no trailing whitespace", () => {
const { envFile } = runScript(workDir, {
PROXY_SERVER_HTTPS_CONNECTION: "false",
diff --git a/packages/graph-explorer/index.html b/packages/graph-explorer/index.html
index 08c10323e..b694c2655 100644
--- a/packages/graph-explorer/index.html
+++ b/packages/graph-explorer/index.html
@@ -1,6 +1,7 @@
+
diff --git a/packages/graph-explorer/src/connector/LoggerConnector.test.ts b/packages/graph-explorer/src/connector/LoggerConnector.test.ts
index b0fc46e6d..8b5eb88cd 100644
--- a/packages/graph-explorer/src/connector/LoggerConnector.test.ts
+++ b/packages/graph-explorer/src/connector/LoggerConnector.test.ts
@@ -15,52 +15,73 @@ describe("ClientLoggerConnector", () => {
});
describe("ServerLoggerConnector", () => {
- test("should send logs to the server", () => {
+ test("should send logs to the server via relative URL", () => {
const mockFetch = vi.fn().mockResolvedValue({});
vi.stubGlobal("fetch", mockFetch);
- const connector = new ServerLoggerConnector("https://example.com/");
+ const connector = new ServerLoggerConnector(
+ "https://example.com/explorer/",
+ );
connector.error("error msg");
- expect(mockFetch).toHaveBeenCalledWith("https://example.com/logger", {
- method: "POST",
- headers: { level: "error", message: JSON.stringify("error msg") },
- });
+ expect(mockFetch).toHaveBeenCalledWith(
+ expect.objectContaining({ href: "https://example.com/logger" }),
+ {
+ method: "POST",
+ headers: { level: "error", message: JSON.stringify("error msg") },
+ },
+ );
connector.warn("warn msg");
- expect(mockFetch).toHaveBeenCalledWith("https://example.com/logger", {
- method: "POST",
- headers: { level: "warn", message: JSON.stringify("warn msg") },
- });
+ expect(mockFetch).toHaveBeenCalledWith(
+ expect.objectContaining({ href: "https://example.com/logger" }),
+ {
+ method: "POST",
+ headers: { level: "warn", message: JSON.stringify("warn msg") },
+ },
+ );
connector.info("info msg");
- expect(mockFetch).toHaveBeenCalledWith("https://example.com/logger", {
- method: "POST",
- headers: { level: "info", message: JSON.stringify("info msg") },
- });
+ expect(mockFetch).toHaveBeenCalledWith(
+ expect.objectContaining({ href: "https://example.com/logger" }),
+ {
+ method: "POST",
+ headers: { level: "info", message: JSON.stringify("info msg") },
+ },
+ );
connector.debug("debug msg");
- expect(mockFetch).toHaveBeenCalledWith("https://example.com/logger", {
- method: "POST",
- headers: { level: "debug", message: JSON.stringify("debug msg") },
- });
+ expect(mockFetch).toHaveBeenCalledWith(
+ expect.objectContaining({ href: "https://example.com/logger" }),
+ {
+ method: "POST",
+ headers: { level: "debug", message: JSON.stringify("debug msg") },
+ },
+ );
connector.trace("trace msg");
- expect(mockFetch).toHaveBeenCalledWith("https://example.com/logger", {
- method: "POST",
- headers: { level: "trace", message: JSON.stringify("trace msg") },
- });
+ expect(mockFetch).toHaveBeenCalledWith(
+ expect.objectContaining({ href: "https://example.com/logger" }),
+ {
+ method: "POST",
+ headers: { level: "trace", message: JSON.stringify("trace msg") },
+ },
+ );
});
- test("should strip trailing slash from connection URL", () => {
+ test("should resolve logger path relative to baseURI", () => {
const mockFetch = vi.fn().mockResolvedValue({});
vi.stubGlobal("fetch", mockFetch);
- const connector = new ServerLoggerConnector("https://example.com/");
+ const connector = new ServerLoggerConnector(
+ "https://example.com/proxy/9250/explorer/",
+ );
connector.info("test");
expect(mockFetch).toHaveBeenCalledWith(
- "https://example.com/logger",
+ expect.objectContaining({
+ href: "https://example.com/proxy/9250/logger",
+ }),
expect.any(Object),
);
});
diff --git a/packages/graph-explorer/src/connector/LoggerConnector.ts b/packages/graph-explorer/src/connector/LoggerConnector.ts
index 9edc2a4bb..420172262 100644
--- a/packages/graph-explorer/src/connector/LoggerConnector.ts
+++ b/packages/graph-explorer/src/connector/LoggerConnector.ts
@@ -1,5 +1,7 @@
import { logger } from "@/utils";
+import { apiUrl } from "./utils/apiUrl";
+
export type LogLevel = "error" | "warn" | "info" | "debug" | "trace";
export interface LoggerConnector {
@@ -10,14 +12,13 @@ export interface LoggerConnector {
trace(message: unknown): void;
}
-/** Sends log messages to the server in the connection configuration. */
+/** Sends log messages to the server via relative URL. */
export class ServerLoggerConnector implements LoggerConnector {
- private readonly _baseUrl: string;
+ private readonly _baseURI: string | undefined;
private readonly _clientLogger: ClientLoggerConnector;
- constructor(connectionUrl: string) {
- const url = connectionUrl.replace(/\/$/, "");
- this._baseUrl = `${url}/logger`;
+ constructor(baseURI?: string) {
+ this._baseURI = baseURI;
this._clientLogger = new ClientLoggerConnector();
}
@@ -47,7 +48,7 @@ export class ServerLoggerConnector implements LoggerConnector {
}
private _sendLog(level: LogLevel, message: unknown) {
- return fetch(this._baseUrl, {
+ return fetch(apiUrl("logger", this._baseURI), {
method: "POST",
headers: {
level,
diff --git a/packages/graph-explorer/src/connector/emptyExplorer.ts b/packages/graph-explorer/src/connector/emptyExplorer.ts
index fd4a28220..bb0dacad2 100644
--- a/packages/graph-explorer/src/connector/emptyExplorer.ts
+++ b/packages/graph-explorer/src/connector/emptyExplorer.ts
@@ -6,10 +6,8 @@ import type { Explorer } from "./useGEFetchTypes";
*/
export const emptyExplorer: Explorer = {
connection: {
- url: "",
graphDbUrl: "",
queryEngine: "gremlin",
- proxyConnection: false,
awsAuthEnabled: false,
},
fetchSchema: async () => {
diff --git a/packages/graph-explorer/src/connector/fetchDatabaseRequest.test.ts b/packages/graph-explorer/src/connector/fetchDatabaseRequest.test.ts
index 4a73383aa..b512b05ee 100644
--- a/packages/graph-explorer/src/connector/fetchDatabaseRequest.test.ts
+++ b/packages/graph-explorer/src/connector/fetchDatabaseRequest.test.ts
@@ -8,10 +8,8 @@ function createConnection(
overrides?: Partial,
): NormalizedConnection {
return {
- url: "http://localhost:8182",
queryEngine: "gremlin",
graphDbUrl: "",
- proxyConnection: false,
awsAuthEnabled: false,
...overrides,
};
@@ -104,10 +102,9 @@ describe("fetchDatabaseRequest", () => {
});
describe("header construction", () => {
- it("sets proxy headers when proxyConnection is true", async () => {
+ it("sets graph-db-connection-url header", async () => {
mockFetch.mockResolvedValue(jsonResponse({}));
const conn = createConnection({
- proxyConnection: true,
graphDbUrl: "https://my-neptune:8182",
});
@@ -124,7 +121,7 @@ describe("fetchDatabaseRequest", () => {
it("sets db-query-logging-enabled based on allowLoggingDbQuery", async () => {
mockFetch.mockResolvedValue(jsonResponse({}));
- const conn = createConnection({ proxyConnection: true });
+ const conn = createConnection({ graphDbUrl: "https://db:8182" });
const flags = createFeatureFlags({ allowLoggingDbQuery: true });
await fetchDatabaseRequest(conn, flags, "/query", { method: "POST" });
@@ -162,7 +159,22 @@ describe("fetchDatabaseRequest", () => {
expect(headers["service-type"]).toBe("neptune-db");
});
- it("does not set proxy or AWS headers when both are disabled", async () => {
+ it("always sends graph-db-connection-url header", async () => {
+ mockFetch.mockResolvedValue(jsonResponse({}));
+ const conn = createConnection({
+ graphDbUrl: "https://my-db:8182",
+ });
+
+ await fetchDatabaseRequest(conn, featureFlags, "/query", {
+ method: "POST",
+ });
+
+ const headers = mockFetch.mock.calls[0][1].headers;
+ expect(headers["graph-db-connection-url"]).toBe("https://my-db:8182");
+ expect(headers["db-query-logging-enabled"]).toBe("false");
+ });
+
+ it("does not set AWS headers when awsAuthEnabled is disabled", async () => {
mockFetch.mockResolvedValue(jsonResponse({}));
await fetchDatabaseRequest(connection, featureFlags, "/query", {
@@ -170,14 +182,13 @@ describe("fetchDatabaseRequest", () => {
});
const headers = mockFetch.mock.calls[0][1].headers;
- expect(headers).not.toHaveProperty("graph-db-connection-url");
expect(headers).not.toHaveProperty("aws-neptune-region");
expect(headers).not.toHaveProperty("service-type");
});
it("merges caller-provided headers with auth headers", async () => {
mockFetch.mockResolvedValue(jsonResponse({}));
- const conn = createConnection({ proxyConnection: true });
+ const conn = createConnection({ graphDbUrl: "https://db:8182" });
await fetchDatabaseRequest(conn, featureFlags, "/query", {
method: "POST",
diff --git a/packages/graph-explorer/src/connector/fetchDatabaseRequest.ts b/packages/graph-explorer/src/connector/fetchDatabaseRequest.ts
index 80696820f..e80e36d2d 100644
--- a/packages/graph-explorer/src/connector/fetchDatabaseRequest.ts
+++ b/packages/graph-explorer/src/connector/fetchDatabaseRequest.ts
@@ -53,12 +53,10 @@ function getAuthHeaders(
typeHeaders: HeadersInit | undefined,
) {
const headers: Record = {};
- if (connection.proxyConnection) {
- headers["graph-db-connection-url"] = connection.graphDbUrl || "";
- headers["db-query-logging-enabled"] = String(
- featureFlags.allowLoggingDbQuery,
- );
- }
+ headers["graph-db-connection-url"] = connection.graphDbUrl || "";
+ headers["db-query-logging-enabled"] = String(
+ featureFlags.allowLoggingDbQuery,
+ );
if (connection.awsAuthEnabled) {
headers["aws-neptune-region"] = connection.awsRegion || "";
headers["service-type"] = connection.serviceType || DEFAULT_SERVICE_TYPE;
diff --git a/packages/graph-explorer/src/connector/gremlin/gremlinExplorer.ts b/packages/graph-explorer/src/connector/gremlin/gremlinExplorer.ts
index ef244a554..225b90d8a 100644
--- a/packages/graph-explorer/src/connector/gremlin/gremlinExplorer.ts
+++ b/packages/graph-explorer/src/connector/gremlin/gremlinExplorer.ts
@@ -9,6 +9,7 @@ import type { Explorer, ExplorerRequestOptions } from "../useGEFetchTypes";
import type { GraphSummary, GremlinFetch } from "./types";
import { fetchDatabaseRequest } from "../fetchDatabaseRequest";
+import { apiUrl } from "../utils/apiUrl";
import { edgeDetails } from "./edgeDetails";
import fetchEdgeConnections from "./fetchEdgeConnections";
import fetchNeighbors from "./fetchNeighbors";
@@ -23,6 +24,7 @@ function _gremlinFetch(
connection: NormalizedConnection,
featureFlags: FeatureFlags,
options?: ExplorerRequestOptions,
+ baseURI?: string,
): GremlinFetch {
return async (queryTemplate: string) => {
logger.debug(queryTemplate);
@@ -31,14 +33,14 @@ function _gremlinFetch(
"Content-Type": "application/json",
Accept: "application/vnd.gremlin-v3.0+json",
};
- if (options?.queryId && connection.proxyConnection === true) {
+ if (options?.queryId) {
headers.queryId = options.queryId;
}
return fetchDatabaseRequest(
connection,
featureFlags,
- `${connection.url}/gremlin`,
+ apiUrl("gremlin", baseURI),
{
method: "POST",
headers,
@@ -53,12 +55,13 @@ async function fetchSummary(
connection: NormalizedConnection,
featureFlags: FeatureFlags,
options?: RequestInit,
+ baseURI?: string,
) {
try {
const response = await fetchDatabaseRequest(
connection,
featureFlags,
- `${connection.url}/pg/statistics/summary?mode=detailed`,
+ apiUrl("pg/statistics/summary?mode=detailed", baseURI),
{
method: "GET",
...options,
@@ -76,15 +79,21 @@ async function fetchSummary(
export function createGremlinExplorer(
connection: NormalizedConnection,
featureFlags: FeatureFlags,
+ baseURI?: string,
): Explorer {
const remoteLogger = createLoggerFromConnection(connection);
return {
connection: connection,
async fetchSchema(options) {
remoteLogger.info("[Gremlin Explorer] Fetching schema...");
- const summary = await fetchSummary(connection, featureFlags, options);
+ const summary = await fetchSummary(
+ connection,
+ featureFlags,
+ options,
+ baseURI,
+ );
return fetchSchema(
- _gremlinFetch(connection, featureFlags, options),
+ _gremlinFetch(connection, featureFlags, options, baseURI),
remoteLogger,
summary,
);
@@ -92,21 +101,21 @@ export function createGremlinExplorer(
async fetchVertexCountsByType(req, options) {
remoteLogger.info("[Gremlin Explorer] Fetching vertex counts by type...");
return fetchVertexTypeCounts(
- _gremlinFetch(connection, featureFlags, options),
+ _gremlinFetch(connection, featureFlags, options, baseURI),
req,
);
},
async fetchNeighbors(req, options) {
remoteLogger.info("[Gremlin Explorer] Fetching neighbors...");
return fetchNeighbors(
- _gremlinFetch(connection, featureFlags, options),
+ _gremlinFetch(connection, featureFlags, options, baseURI),
req,
);
},
async neighborCounts(req, options) {
remoteLogger.info("[Gremlin Explorer] Fetching neighbors count...");
return neighborCounts(
- _gremlinFetch(connection, featureFlags, options),
+ _gremlinFetch(connection, featureFlags, options, baseURI),
req,
);
},
@@ -116,7 +125,7 @@ export function createGremlinExplorer(
remoteLogger.info("[Gremlin Explorer] Fetching keyword search...");
return keywordSearch(
- _gremlinFetch(connection, featureFlags, options),
+ _gremlinFetch(connection, featureFlags, options, baseURI),
req,
);
},
@@ -126,7 +135,7 @@ export function createGremlinExplorer(
remoteLogger.info("[Gremlin Explorer] Fetching vertex details...");
const result = await vertexDetails(
- _gremlinFetch(connection, featureFlags, options),
+ _gremlinFetch(connection, featureFlags, options, baseURI),
req,
);
return result;
@@ -137,7 +146,7 @@ export function createGremlinExplorer(
remoteLogger.info("[Gremlin Explorer] Fetching edge details...");
const result = await edgeDetails(
- _gremlinFetch(connection, featureFlags, options),
+ _gremlinFetch(connection, featureFlags, options, baseURI),
req,
);
return result;
@@ -147,7 +156,7 @@ export function createGremlinExplorer(
options.queryId = v4();
remoteLogger.info("[Gremlin Explorer] Fetching raw query...");
const result = await rawQuery(
- _gremlinFetch(connection, featureFlags, options),
+ _gremlinFetch(connection, featureFlags, options, baseURI),
req,
);
return result;
@@ -155,7 +164,7 @@ export function createGremlinExplorer(
async fetchEdgeConnections(req, options) {
remoteLogger.info("[Gremlin Explorer] Fetching edge connections...");
return fetchEdgeConnections(
- _gremlinFetch(connection, featureFlags, options),
+ _gremlinFetch(connection, featureFlags, options, baseURI),
req,
);
},
diff --git a/packages/graph-explorer/src/connector/openCypher/openCypherExplorer.ts b/packages/graph-explorer/src/connector/openCypher/openCypherExplorer.ts
index 104d92c96..3c7c999d6 100644
--- a/packages/graph-explorer/src/connector/openCypher/openCypherExplorer.ts
+++ b/packages/graph-explorer/src/connector/openCypher/openCypherExplorer.ts
@@ -8,6 +8,7 @@ import type { Explorer, ExplorerRequestOptions } from "../useGEFetchTypes";
import type { GraphSummary } from "./types";
import { fetchDatabaseRequest } from "../fetchDatabaseRequest";
+import { apiUrl } from "../utils/apiUrl";
import { edgeDetails } from "./edgeDetails";
import fetchEdgeConnections from "./fetchEdgeConnections";
import fetchNeighbors from "./fetchNeighbors";
@@ -22,13 +23,14 @@ function _openCypherFetch(
connection: NormalizedConnection,
featureFlags: FeatureFlags,
options?: ExplorerRequestOptions,
+ baseURI?: string,
) {
return async (queryTemplate: string) => {
logger.debug(queryTemplate);
return fetchDatabaseRequest(
connection,
featureFlags,
- `${connection.url}/openCypher`,
+ apiUrl("openCypher", baseURI),
{
method: "POST",
headers: {
@@ -44,6 +46,7 @@ function _openCypherFetch(
export function createOpenCypherExplorer(
connection: NormalizedConnection,
featureFlags: FeatureFlags,
+ baseURI?: string,
): Explorer {
const remoteLogger = createLoggerFromConnection(connection);
const serviceType = connection.serviceType || DEFAULT_SERVICE_TYPE;
@@ -56,9 +59,10 @@ export function createOpenCypherExplorer(
connection,
featureFlags,
options,
+ baseURI,
);
return fetchSchema(
- _openCypherFetch(connection, featureFlags, options),
+ _openCypherFetch(connection, featureFlags, options, baseURI),
remoteLogger,
summary,
);
@@ -68,53 +72,56 @@ export function createOpenCypherExplorer(
"[openCypher Explorer] Fetching vertex counts by type...",
);
return fetchVertexTypeCounts(
- _openCypherFetch(connection, featureFlags, options),
+ _openCypherFetch(connection, featureFlags, options, baseURI),
req,
);
},
async fetchNeighbors(req, options) {
remoteLogger.info("[openCypher Explorer] Fetching neighbors...");
return fetchNeighbors(
- _openCypherFetch(connection, featureFlags, options),
+ _openCypherFetch(connection, featureFlags, options, baseURI),
req,
);
},
async neighborCounts(req, options) {
remoteLogger.info("[openCypher Explorer] Fetching neighbors count...");
return neighborCounts(
- _openCypherFetch(connection, featureFlags, options),
+ _openCypherFetch(connection, featureFlags, options, baseURI),
req,
);
},
async keywordSearch(req, options) {
remoteLogger.info("[openCypher Explorer] Fetching keyword search...");
return keywordSearch(
- _openCypherFetch(connection, featureFlags, options),
+ _openCypherFetch(connection, featureFlags, options, baseURI),
req,
);
},
async vertexDetails(req, options) {
remoteLogger.info("[openCypher Explorer] Fetching vertex details...");
return vertexDetails(
- _openCypherFetch(connection, featureFlags, options),
+ _openCypherFetch(connection, featureFlags, options, baseURI),
req,
);
},
async edgeDetails(req, options) {
remoteLogger.info("[openCypher Explorer] Fetching edge details...");
return edgeDetails(
- _openCypherFetch(connection, featureFlags, options),
+ _openCypherFetch(connection, featureFlags, options, baseURI),
req,
);
},
async rawQuery(req, options) {
remoteLogger.info("[openCypher Explorer] Fetching raw query...");
- return rawQuery(_openCypherFetch(connection, featureFlags, options), req);
+ return rawQuery(
+ _openCypherFetch(connection, featureFlags, options, baseURI),
+ req,
+ );
},
async fetchEdgeConnections(req, options) {
remoteLogger.info("[openCypher Explorer] Fetching edge connections...");
return fetchEdgeConnections(
- _openCypherFetch(connection, featureFlags, options),
+ _openCypherFetch(connection, featureFlags, options, baseURI),
req,
);
},
@@ -126,12 +133,13 @@ async function fetchSummary(
connection: NormalizedConnection,
featureFlags: FeatureFlags,
options?: RequestInit,
+ baseURI?: string,
) {
try {
const endpoint =
serviceType === DEFAULT_SERVICE_TYPE
- ? `${connection.url}/pg/statistics/summary?mode=detailed`
- : `${connection.url}/summary?mode=detailed`;
+ ? apiUrl("pg/statistics/summary?mode=detailed", baseURI)
+ : apiUrl("summary?mode=detailed", baseURI);
const response = await fetchDatabaseRequest(
connection,
featureFlags,
diff --git a/packages/graph-explorer/src/connector/sparql/sparqlExplorer.ts b/packages/graph-explorer/src/connector/sparql/sparqlExplorer.ts
index 44160068d..23d790e2a 100644
--- a/packages/graph-explorer/src/connector/sparql/sparqlExplorer.ts
+++ b/packages/graph-explorer/src/connector/sparql/sparqlExplorer.ts
@@ -18,6 +18,7 @@ import type {
} from "./types";
import { fetchDatabaseRequest } from "../fetchDatabaseRequest";
+import { apiUrl } from "../utils/apiUrl";
import { edgeDetails } from "./edgeDetails";
import fetchEdgeConnections from "./fetchEdgeConnections";
import fetchNeighbors from "./fetchNeighbors";
@@ -35,26 +36,26 @@ function _sparqlFetch(
connection: NormalizedConnection,
featureFlags: FeatureFlags,
options?: ExplorerRequestOptions,
+ baseURI?: string,
) {
return async (queryTemplate: string) => {
logger.debug(queryTemplate);
const body = `query=${encodeURIComponent(queryTemplate)}`;
const queryId = options?.queryId;
- const headers: Record =
- queryId && connection.proxyConnection === true
- ? {
- accept: "application/sparql-results+json",
- "Content-Type": "application/x-www-form-urlencoded",
- queryId: queryId,
- }
- : {
- accept: "application/sparql-results+json",
- "Content-Type": "application/x-www-form-urlencoded",
- };
+ const headers: Record = queryId
+ ? {
+ accept: "application/sparql-results+json",
+ "Content-Type": "application/x-www-form-urlencoded",
+ queryId: queryId,
+ }
+ : {
+ accept: "application/sparql-results+json",
+ "Content-Type": "application/x-www-form-urlencoded",
+ };
return fetchDatabaseRequest(
connection,
featureFlags,
- `${connection.url}/sparql`,
+ apiUrl("sparql", baseURI),
{
method: "POST",
headers,
@@ -69,12 +70,13 @@ async function fetchSummary(
connection: NormalizedConnection,
featureFlags: FeatureFlags,
options?: RequestInit,
+ baseURI?: string,
) {
try {
const response = await fetchDatabaseRequest(
connection,
featureFlags,
- `${connection.url}/rdf/statistics/summary?mode=detailed`,
+ apiUrl("rdf/statistics/summary?mode=detailed", baseURI),
{
method: "GET",
...options,
@@ -93,15 +95,21 @@ export function createSparqlExplorer(
connection: NormalizedConnection,
featureFlags: FeatureFlags,
blankNodes: BlankNodesMap,
+ baseURI?: string,
): Explorer {
const remoteLogger = createLoggerFromConnection(connection);
return {
connection: connection,
async fetchSchema(options) {
remoteLogger.info("[SPARQL Explorer] Fetching schema...");
- const summary = await fetchSummary(connection, featureFlags, options);
+ const summary = await fetchSummary(
+ connection,
+ featureFlags,
+ options,
+ baseURI,
+ );
return fetchSchema(
- _sparqlFetch(connection, featureFlags, options),
+ _sparqlFetch(connection, featureFlags, options, baseURI),
remoteLogger,
summary,
);
@@ -109,7 +117,7 @@ export function createSparqlExplorer(
async fetchVertexCountsByType(req, options) {
remoteLogger.info("[SPARQL Explorer] Fetching vertex counts by type...");
return fetchClassCounts(
- _sparqlFetch(connection, featureFlags, options),
+ _sparqlFetch(connection, featureFlags, options, baseURI),
req,
);
},
@@ -132,7 +140,7 @@ export function createSparqlExplorer(
}
const response = await fetchNeighbors(
- _sparqlFetch(connection, featureFlags, options),
+ _sparqlFetch(connection, featureFlags, options, baseURI),
request,
);
const vertices = replaceBlankNodeFromNeighbors(
@@ -145,7 +153,7 @@ export function createSparqlExplorer(
async neighborCounts(req, options) {
remoteLogger.info("[SPARQL Explorer] Fetching neighbor counts...");
return neighborCounts(
- _sparqlFetch(connection, featureFlags, options),
+ _sparqlFetch(connection, featureFlags, options, baseURI),
req,
blankNodes,
);
@@ -166,7 +174,7 @@ export function createSparqlExplorer(
};
const response = await keywordSearch(
- _sparqlFetch(connection, featureFlags, options),
+ _sparqlFetch(connection, featureFlags, options, baseURI),
reqParams,
);
const vertices = replaceBlankNodeFromSearch(
@@ -180,7 +188,7 @@ export function createSparqlExplorer(
async vertexDetails(req, options) {
remoteLogger.info("[SPARQL Explorer] Fetching vertex details...");
return await vertexDetails(
- _sparqlFetch(connection, featureFlags, options),
+ _sparqlFetch(connection, featureFlags, options, baseURI),
req,
);
},
@@ -190,14 +198,14 @@ export function createSparqlExplorer(
async rawQuery(req, options) {
remoteLogger.info("[SPARQL Explorer] Fetching raw query...");
return await rawQuery(
- _sparqlFetch(connection, featureFlags, options),
+ _sparqlFetch(connection, featureFlags, options, baseURI),
req,
);
},
async fetchEdgeConnections(req, options) {
remoteLogger.info("[SPARQL Explorer] Fetching edge connections...");
return fetchEdgeConnections(
- _sparqlFetch(connection, featureFlags, options),
+ _sparqlFetch(connection, featureFlags, options, baseURI),
req,
);
},
diff --git a/packages/graph-explorer/src/connector/utils/apiUrl.test.ts b/packages/graph-explorer/src/connector/utils/apiUrl.test.ts
new file mode 100644
index 000000000..6b945e819
--- /dev/null
+++ b/packages/graph-explorer/src/connector/utils/apiUrl.test.ts
@@ -0,0 +1,43 @@
+import { apiUrl } from "./apiUrl";
+
+describe("apiUrl", () => {
+ test("resolves a simple endpoint relative to baseURI", () => {
+ const result = apiUrl("gremlin", "http://localhost/explorer/");
+ expect(result.href).toBe("http://localhost/gremlin");
+ });
+
+ test("resolves sparql endpoint", () => {
+ const result = apiUrl("sparql", "http://localhost/explorer/");
+ expect(result.href).toBe("http://localhost/sparql");
+ });
+
+ test("resolves openCypher endpoint", () => {
+ const result = apiUrl("openCypher", "http://localhost/explorer/");
+ expect(result.href).toBe("http://localhost/openCypher");
+ });
+
+ test("resolves endpoint with nested base path", () => {
+ const result = apiUrl("gremlin", "http://localhost/proxy/9250/explorer/");
+ expect(result.href).toBe("http://localhost/proxy/9250/gremlin");
+ });
+
+ test("resolves endpoint with query parameters", () => {
+ const result = apiUrl(
+ "pg/statistics/summary?mode=detailed",
+ "http://localhost/explorer/",
+ );
+ expect(result.href).toBe(
+ "http://localhost/pg/statistics/summary?mode=detailed",
+ );
+ });
+
+ test("resolves logger endpoint", () => {
+ const result = apiUrl("logger", "http://localhost/explorer/");
+ expect(result.href).toBe("http://localhost/logger");
+ });
+
+ test("resolves defaultConnection endpoint", () => {
+ const result = apiUrl("defaultConnection", "http://localhost/explorer/");
+ expect(result.href).toBe("http://localhost/defaultConnection");
+ });
+});
diff --git a/packages/graph-explorer/src/connector/utils/apiUrl.ts b/packages/graph-explorer/src/connector/utils/apiUrl.ts
new file mode 100644
index 000000000..08bb0d004
--- /dev/null
+++ b/packages/graph-explorer/src/connector/utils/apiUrl.ts
@@ -0,0 +1,9 @@
+/** Resolves an API endpoint path relative to the given baseURI.
+ * The server mounts static files at `/explorer` and API routes at `/`,
+ * so `../endpoint` from the static directory resolves to the API root. */
+export function apiUrl(
+ endpoint: string,
+ baseURI: string = document.baseURI,
+): URL {
+ return new URL(`../${endpoint}`, baseURI);
+}
diff --git a/packages/graph-explorer/src/core/AppStatusLoader.tsx b/packages/graph-explorer/src/core/AppStatusLoader.tsx
index 36535788f..4c3c46493 100644
--- a/packages/graph-explorer/src/core/AppStatusLoader.tsx
+++ b/packages/graph-explorer/src/core/AppStatusLoader.tsx
@@ -27,7 +27,7 @@ function LoadDefaultConfig({ children }: PropsWithChildren) {
const defaultConfigQuery = useQuery({
queryKey: ["default-connection"],
- queryFn: fetchDefaultConnection,
+ queryFn: () => fetchDefaultConnection(),
staleTime: Infinity,
// Run the query only if the store is loaded and there are no configs
enabled: configuration.size === 0,
diff --git a/packages/graph-explorer/src/core/StateProvider/configuration.test.ts b/packages/graph-explorer/src/core/StateProvider/configuration.test.ts
index c71b6ba13..c2bcf2698 100644
--- a/packages/graph-explorer/src/core/StateProvider/configuration.test.ts
+++ b/packages/graph-explorer/src/core/StateProvider/configuration.test.ts
@@ -24,6 +24,7 @@ import {
getDefaultEdgeTypeConfig,
getDefaultVertexTypeConfig,
mergeConfiguration,
+ migrateLegacyConnection,
normalizeConnection,
type NormalizedConnection,
patchToRemoveDisplayLabel,
@@ -31,10 +32,8 @@ import {
/** The default empty connection values when no value is provided. */
const defaultEmptyConnection: NormalizedConnection = {
- url: "",
graphDbUrl: "",
queryEngine: "gremlin",
- proxyConnection: false,
awsAuthEnabled: false,
};
@@ -107,7 +106,6 @@ describe("mergedConfiguration", () => {
connection: {
...defaultEmptyConnection,
...config.connection,
- url: config.connection?.url ?? "",
graphDbUrl: config.connection?.graphDbUrl ?? "",
},
schema: expectedSchema,
@@ -164,7 +162,6 @@ describe("mergedConfiguration", () => {
connection: {
...defaultEmptyConnection,
...config.connection,
- url: config.connection?.url ?? "",
graphDbUrl: config.connection?.graphDbUrl ?? "",
},
schema: expectedSchema,
@@ -329,51 +326,23 @@ describe("patchToRemoveDisplayLabel", () => {
});
describe("normalizeConnection", () => {
- test("should remove trailing slash from url", () => {
- const result = normalizeConnection({ url: "https://example.com/" });
- expect(result.url).toBe("https://example.com");
+ test("should remove trailing slash from graphDbUrl", () => {
+ const result = normalizeConnection({ graphDbUrl: "https://example.com/" });
+ expect(result.graphDbUrl).toBe("https://example.com");
});
test("should default queryEngine to gremlin", () => {
- const result = normalizeConnection({ url: "https://example.com" });
+ const result = normalizeConnection({ graphDbUrl: "https://example.com" });
expect(result.queryEngine).toBe("gremlin");
});
- test("should default proxyConnection to true when graphDbUrl is present", () => {
- const result = normalizeConnection({
- url: "https://proxy.com",
- graphDbUrl: "https://db.com",
- });
- expect(result.proxyConnection).toBe(true);
- });
-
- test("should default proxyConnection to false when graphDbUrl is absent", () => {
- const result = normalizeConnection({ url: "https://example.com" });
- expect(result.proxyConnection).toBe(false);
- });
-
test("should default awsAuthEnabled to false", () => {
- const result = normalizeConnection({ url: "https://example.com" });
+ const result = normalizeConnection({ graphDbUrl: "https://example.com" });
expect(result.awsAuthEnabled).toBe(false);
});
- test("should preserve path in url", () => {
- const result = normalizeConnection({
- url: "http://localhost:9999/blazegraph/namespace/kb",
- });
- expect(result.url).toBe("http://localhost:9999/blazegraph/namespace/kb");
- });
-
- test("should remove only trailing slash from url with path", () => {
- const result = normalizeConnection({
- url: "http://localhost:9999/blazegraph/namespace/kb/",
- });
- expect(result.url).toBe("http://localhost:9999/blazegraph/namespace/kb");
- });
-
test("should preserve path in graphDbUrl", () => {
const result = normalizeConnection({
- url: "http://proxy:8080",
graphDbUrl: "http://blazegraph:9999/blazegraph/namespace/kb",
});
expect(result.graphDbUrl).toBe(
@@ -383,13 +352,101 @@ describe("normalizeConnection", () => {
test("should remove only trailing slash from graphDbUrl with path", () => {
const result = normalizeConnection({
- url: "http://proxy:8080",
graphDbUrl: "http://blazegraph:9999/blazegraph/namespace/kb/",
});
expect(result.graphDbUrl).toBe(
"http://blazegraph:9999/blazegraph/namespace/kb",
);
});
+
+ test("should migrate legacy connection with url and proxyConnection=true", () => {
+ const result = normalizeConnection({
+ url: "https://proxy.com",
+ proxyConnection: true,
+ graphDbUrl: "https://db.com",
+ } as any);
+ expect(result.graphDbUrl).toBe("https://db.com");
+ });
+
+ test("should migrate legacy connection with url and proxyConnection=false", () => {
+ const result = normalizeConnection({
+ url: "https://my-neptune:8182",
+ proxyConnection: false,
+ } as any);
+ expect(result.graphDbUrl).toBe("https://my-neptune:8182");
+ });
+});
+
+describe("migrateLegacyConnection", () => {
+ test("should use graphDbUrl directly when proxyConnection is true", () => {
+ const result = migrateLegacyConnection({
+ url: "https://proxy.example.com",
+ proxyConnection: true,
+ graphDbUrl: "https://my-neptune:8182",
+ });
+ expect(result.graphDbUrl).toBe("https://my-neptune:8182");
+ });
+
+ test("should use url as graphDbUrl when proxyConnection is false", () => {
+ const result = migrateLegacyConnection({
+ url: "https://my-neptune:8182",
+ proxyConnection: false,
+ });
+ expect(result.graphDbUrl).toBe("https://my-neptune:8182");
+ });
+
+ test("should use url as graphDbUrl when proxyConnection is absent and no graphDbUrl", () => {
+ const result = migrateLegacyConnection({
+ url: "https://my-neptune:8182",
+ });
+ expect(result.graphDbUrl).toBe("https://my-neptune:8182");
+ });
+
+ test("should not include proxyConnection in result", () => {
+ const result = migrateLegacyConnection({
+ url: "https://proxy.com",
+ proxyConnection: true,
+ graphDbUrl: "https://db.com",
+ });
+ expect(result).not.toHaveProperty("proxyConnection");
+ });
+
+ test("should not include url in result", () => {
+ const result = migrateLegacyConnection({
+ url: "https://proxy.com",
+ proxyConnection: true,
+ graphDbUrl: "https://db.com",
+ });
+ expect(result).not.toHaveProperty("url");
+ });
+
+ test("should preserve other connection properties", () => {
+ const result = migrateLegacyConnection({
+ url: "https://proxy.com",
+ proxyConnection: true,
+ graphDbUrl: "https://db.com",
+ queryEngine: "sparql",
+ awsAuthEnabled: true,
+ awsRegion: "us-east-1",
+ serviceType: "neptune-graph",
+ fetchTimeoutMs: 30000,
+ nodeExpansionLimit: 100,
+ });
+ expect(result.queryEngine).toBe("sparql");
+ expect(result.awsAuthEnabled).toBe(true);
+ expect(result.awsRegion).toBe("us-east-1");
+ expect(result.serviceType).toBe("neptune-graph");
+ expect(result.fetchTimeoutMs).toBe(30000);
+ expect(result.nodeExpansionLimit).toBe(100);
+ });
+
+ test("should pass through a connection that already has graphDbUrl and no url", () => {
+ const result = migrateLegacyConnection({
+ graphDbUrl: "https://db.com",
+ queryEngine: "gremlin",
+ });
+ expect(result.graphDbUrl).toBe("https://db.com");
+ });
});
describe("getDefaultVertexTypeConfig", () => {
diff --git a/packages/graph-explorer/src/core/StateProvider/configuration.ts b/packages/graph-explorer/src/core/StateProvider/configuration.ts
index 6bccbfd82..647870896 100644
--- a/packages/graph-explorer/src/core/StateProvider/configuration.ts
+++ b/packages/graph-explorer/src/core/StateProvider/configuration.ts
@@ -1,4 +1,4 @@
-import type { ConnectionConfig } from "@shared/types";
+import type { ConnectionConfig, LegacyConnectionConfig } from "@shared/types";
import { atom } from "jotai";
import { selectAtom } from "jotai/utils";
@@ -98,7 +98,9 @@ export function mergeConfiguration(
return {
id: currentConfig.id,
displayLabel: currentConfig.displayLabel,
- connection: normalizeConnection(currentConfig.connection || { url: "" }),
+ connection: normalizeConnection(
+ currentConfig.connection || { graphDbUrl: "" },
+ ),
schema: {
vertices: mergedVertices,
edges: mergedEdges,
@@ -115,16 +117,26 @@ export function mergeConfiguration(
};
}
-export function normalizeConnection(connection: ConnectionConfig) {
+/** Migrates a legacy connection (with `url` and `proxyConnection`) to the new
+ * format where only `graphDbUrl` exists. */
+export function migrateLegacyConnection(
+ connection: LegacyConnectionConfig,
+): ConnectionConfig {
+ const { url, proxyConnection, ...rest } = connection;
+ const graphDbUrl = proxyConnection ? connection.graphDbUrl : url;
+ return {
+ ...rest,
+ graphDbUrl: graphDbUrl || connection.graphDbUrl || "",
+ };
+}
+
+export function normalizeConnection(connection: LegacyConnectionConfig) {
+ const migrated = migrateLegacyConnection(connection);
return {
- ...connection,
- // Remove trailing slash
- url: connection.url.replace(/\/$/, "") || "",
- queryEngine: connection.queryEngine || "gremlin",
- graphDbUrl: connection.graphDbUrl?.replace(/\/$/, "") || "",
- proxyConnection:
- connection.proxyConnection ?? connection.graphDbUrl != null,
- awsAuthEnabled: connection.awsAuthEnabled ?? false,
+ ...migrated,
+ graphDbUrl: migrated.graphDbUrl.replace(/\/$/, "") || "",
+ queryEngine: migrated.queryEngine || "gremlin",
+ awsAuthEnabled: migrated.awsAuthEnabled ?? false,
};
}
export type NormalizedConnection = ReturnType;
diff --git a/packages/graph-explorer/src/core/connector.ts b/packages/graph-explorer/src/core/connector.ts
index 5693c6349..1d9613b56 100644
--- a/packages/graph-explorer/src/core/connector.ts
+++ b/packages/graph-explorer/src/core/connector.ts
@@ -73,23 +73,15 @@ export const loggerSelector = atom(get =>
createLoggerFromConnection(get(activeConnectionAtom)),
);
-/** Creates a logger instance that will be remote if the connection is using the
- * proxy server. Otherwise it will be a client only logger. */
+/** Creates a logger instance that sends logs to the server via relative URL. */
export function createLoggerFromConnection(
connection: NormalizedConnection | null,
+ baseURI?: string,
): LoggerConnector {
- // Check for a url and that we are using the proxy server
- if (!connection || !connection.url || connection.proxyConnection !== true) {
- logger.debug(
- "Connection did not contain enough information to create a remote logger, so using a client logger instead",
- connection,
- );
+ if (!connection) {
+ logger.debug("No connection provided, using a client logger instead");
return new ClientLoggerConnector();
}
- logger.debug(
- "Creating a remote server logger using proxy server URL",
- connection.url,
- );
- return new ServerLoggerConnector(connection.url);
+ return new ServerLoggerConnector(baseURI);
}
diff --git a/packages/graph-explorer/src/core/defaultConnection.test.ts b/packages/graph-explorer/src/core/defaultConnection.test.ts
index 30f637b54..e33437fe4 100644
--- a/packages/graph-explorer/src/core/defaultConnection.test.ts
+++ b/packages/graph-explorer/src/core/defaultConnection.test.ts
@@ -13,9 +13,86 @@ import {
import {
DefaultConnectionDataSchema,
+ fetchDefaultConnection,
mapToConnection,
} from "./defaultConnection";
+describe("fetchDefaultConnection", () => {
+ let mockFetch: ReturnType;
+
+ beforeEach(() => {
+ mockFetch = vi.fn();
+ vi.stubGlobal("fetch", mockFetch);
+ });
+
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ });
+
+ test("should fetch from a single relative URL", async () => {
+ mockFetch.mockResolvedValue(
+ new Response(
+ JSON.stringify({
+ GRAPH_EXP_CONNECTION_URL: "https://db.example.com:8182",
+ GRAPH_EXP_GRAPH_TYPE: "gremlin",
+ GRAPH_EXP_IAM: true,
+ GRAPH_EXP_AWS_REGION: "us-east-1",
+ }),
+ { status: 200 },
+ ),
+ );
+
+ const result = await fetchDefaultConnection(
+ "http://localhost/explorer/index.html",
+ );
+
+ expect(mockFetch).toHaveBeenCalledTimes(1);
+ expect(mockFetch).toHaveBeenCalledWith(
+ expect.objectContaining({
+ href: "http://localhost/defaultConnection",
+ }),
+ );
+ expect(result).toHaveLength(1);
+ expect(result[0].connection?.graphDbUrl).toBe(
+ "https://db.example.com:8182",
+ );
+ });
+
+ test("should not fall back to sagemaker path", async () => {
+ mockFetch.mockResolvedValue(new Response("", { status: 404 }));
+
+ const result = await fetchDefaultConnection(
+ "http://localhost/explorer/index.html",
+ );
+
+ expect(mockFetch).toHaveBeenCalledTimes(1);
+ expect(result).toHaveLength(0);
+ });
+
+ test("should return all query engines when none specified", async () => {
+ mockFetch.mockResolvedValue(
+ new Response(
+ JSON.stringify({
+ GRAPH_EXP_CONNECTION_URL: "https://db.example.com:8182",
+ GRAPH_EXP_IAM: false,
+ }),
+ { status: 200 },
+ ),
+ );
+
+ const result = await fetchDefaultConnection(
+ "http://localhost/explorer/index.html",
+ );
+
+ expect(result).toHaveLength(3);
+ expect(result.map(c => c.connection?.queryEngine)).toEqual([
+ "gremlin",
+ "openCypher",
+ "sparql",
+ ]);
+ });
+});
+
describe("mapToConnection", () => {
test("should map default connection data to connection config", () => {
const defaultConnectionData = createRandomDefaultConnectionData();
@@ -25,8 +102,6 @@ describe("mapToConnection", () => {
displayLabel: "Default Connection",
connection: {
graphDbUrl: defaultConnectionData.GRAPH_EXP_CONNECTION_URL,
- url: defaultConnectionData.GRAPH_EXP_PUBLIC_OR_PROXY_ENDPOINT,
- proxyConnection: defaultConnectionData.GRAPH_EXP_USING_PROXY_SERVER,
queryEngine: defaultConnectionData.GRAPH_EXP_GRAPH_TYPE,
awsAuthEnabled: defaultConnectionData.GRAPH_EXP_IAM,
awsRegion: defaultConnectionData.GRAPH_EXP_AWS_REGION,
@@ -50,9 +125,7 @@ describe("DefaultConnectionDataSchema", () => {
const data = {};
const actual = DefaultConnectionDataSchema.parse(data);
expect(actual).toEqual({
- GRAPH_EXP_USING_PROXY_SERVER: false,
GRAPH_EXP_CONNECTION_URL: "",
- GRAPH_EXP_PUBLIC_OR_PROXY_ENDPOINT: "",
GRAPH_EXP_IAM: false,
GRAPH_EXP_AWS_REGION: "",
GRAPH_EXP_SERVICE_TYPE: "neptune-db",
@@ -63,7 +136,6 @@ describe("DefaultConnectionDataSchema", () => {
test("should handle invalid service type", () => {
const data: any = createRandomDefaultConnectionData();
data.GRAPH_EXP_SERVICE_TYPE = createRandomName("serviceType");
- // Make the enum less strict
const actual = DefaultConnectionDataSchema.parse(data);
expect(actual).toEqual({ ...data, GRAPH_EXP_SERVICE_TYPE: "neptune-db" });
});
@@ -71,15 +143,10 @@ describe("DefaultConnectionDataSchema", () => {
test("should handle invalid URLs", () => {
const data: any = createRandomDefaultConnectionData();
data.GRAPH_EXP_CONNECTION_URL = createRandomName("connectionURL");
- data.GRAPH_EXP_PUBLIC_OR_PROXY_ENDPOINT = createRandomName(
- "publicOrProxyEndpoint",
- );
- // Make the enum less strict
const actual = DefaultConnectionDataSchema.parse(data);
expect(actual).toEqual({
...data,
GRAPH_EXP_CONNECTION_URL: "",
- GRAPH_EXP_PUBLIC_OR_PROXY_ENDPOINT: "",
});
});
@@ -94,25 +161,11 @@ describe("DefaultConnectionDataSchema", () => {
"http://blazegraph:9999/blazegraph/namespace/kb",
);
});
-
- test("should preserve path in GRAPH_EXP_PUBLIC_OR_PROXY_ENDPOINT", () => {
- const data = {
- ...createRandomDefaultConnectionData(),
- GRAPH_EXP_PUBLIC_OR_PROXY_ENDPOINT:
- "http://localhost:8080/proxy/explorer",
- };
- const actual = DefaultConnectionDataSchema.parse(data);
- expect(actual.GRAPH_EXP_PUBLIC_OR_PROXY_ENDPOINT).toBe(
- "http://localhost:8080/proxy/explorer",
- );
- });
});
function createRandomDefaultConnectionData() {
return {
- GRAPH_EXP_USING_PROXY_SERVER: createRandomBoolean(),
GRAPH_EXP_CONNECTION_URL: createRandomUrlString(),
- GRAPH_EXP_PUBLIC_OR_PROXY_ENDPOINT: createRandomUrlString(),
GRAPH_EXP_GRAPH_TYPE: createRandomQueryEngine(),
GRAPH_EXP_IAM: createRandomBoolean(),
GRAPH_EXP_AWS_REGION: createRandomAwsRegion(),
diff --git a/packages/graph-explorer/src/core/defaultConnection.ts b/packages/graph-explorer/src/core/defaultConnection.ts
index 775f8246f..34806918c 100644
--- a/packages/graph-explorer/src/core/defaultConnection.ts
+++ b/packages/graph-explorer/src/core/defaultConnection.ts
@@ -1,6 +1,7 @@
import { neptuneServiceTypeOptions, queryEngineOptions } from "@shared/types";
import { z } from "zod";
+import { apiUrl } from "@/connector/utils/apiUrl";
import { DEFAULT_SERVICE_TYPE, logger } from "@/utils";
import type {
@@ -10,9 +11,7 @@ import type {
export const DefaultConnectionDataSchema = z.object({
// Connection info
- GRAPH_EXP_USING_PROXY_SERVER: z.boolean().default(false),
GRAPH_EXP_CONNECTION_URL: z.string().url().catch(""),
- GRAPH_EXP_PUBLIC_OR_PROXY_ENDPOINT: z.string().url().catch(""),
GRAPH_EXP_GRAPH_TYPE: z.enum(queryEngineOptions).optional(),
// IAM auth info
GRAPH_EXP_IAM: z.boolean().default(false),
@@ -28,15 +27,11 @@ export const DefaultConnectionDataSchema = z.object({
export type DefaultConnectionData = z.infer;
-/** Fetches the default connections from multiple possible locations and returns an empty array on failure. */
-export async function fetchDefaultConnection() {
- const defaultConnectionPath = `${location.origin}/defaultConnection`;
- const sagemakerConnectionPath = `${location.origin}/proxy/9250/defaultConnection`;
-
+/** Fetches the default connection from the server and returns an empty array on failure. */
+export async function fetchDefaultConnection(baseURI?: string) {
try {
- const defaultConnection =
- (await fetchDefaultConnectionFor(defaultConnectionPath)) ??
- (await fetchDefaultConnectionFor(sagemakerConnectionPath));
+ const url = apiUrl("defaultConnection", baseURI);
+ const defaultConnection = await fetchDefaultConnectionFor(url);
if (!defaultConnection) {
return [];
@@ -44,12 +39,10 @@ export async function fetchDefaultConnection() {
const config = mapToConnection(defaultConnection);
- // A specific query engine was specified, so just return that
if (config.connection?.queryEngine) {
return [config];
}
- // No query engine was specified, so return all the possible ones
const configs = queryEngineOptions.map(queryEngine => {
return {
...config,
@@ -72,7 +65,7 @@ export async function fetchDefaultConnection() {
/** Attempts to fetch a default connection from the given URL and returns null on a failure. */
export async function fetchDefaultConnectionFor(
- url: string,
+ url: URL | string,
): Promise {
try {
logger.debug("Fetching default connection from", url);
@@ -109,10 +102,8 @@ export function mapToConnection(data: DefaultConnectionData): RawConfiguration {
id: "Default Connection" as ConfigurationId,
displayLabel: "Default Connection",
connection: {
- url: data.GRAPH_EXP_PUBLIC_OR_PROXY_ENDPOINT,
- queryEngine: data.GRAPH_EXP_GRAPH_TYPE,
- proxyConnection: data.GRAPH_EXP_USING_PROXY_SERVER,
graphDbUrl: data.GRAPH_EXP_CONNECTION_URL,
+ queryEngine: data.GRAPH_EXP_GRAPH_TYPE,
awsAuthEnabled: data.GRAPH_EXP_IAM,
awsRegion: data.GRAPH_EXP_AWS_REGION,
serviceType: data.GRAPH_EXP_SERVICE_TYPE,
diff --git a/packages/graph-explorer/src/modules/AvailableConnections/ConnectionRow.tsx b/packages/graph-explorer/src/modules/AvailableConnections/ConnectionRow.tsx
index 2b9fa1909..e6024a4aa 100644
--- a/packages/graph-explorer/src/modules/AvailableConnections/ConnectionRow.tsx
+++ b/packages/graph-explorer/src/modules/AvailableConnections/ConnectionRow.tsx
@@ -24,11 +24,7 @@ function ConnectionRow({
const t = useTranslations();
const setActiveConfig = useSetActiveConfigCallback(connection.id);
- const dbUrl = connection.connection
- ? connection.connection.proxyConnection
- ? connection.connection.graphDbUrl
- : connection.connection.url
- : null;
+ const dbUrl = connection.connection?.graphDbUrl || null;
const graphType = t(
"query-language",
diff --git a/packages/graph-explorer/src/modules/AvailableConnections/useImportConnectionFile.test.tsx b/packages/graph-explorer/src/modules/AvailableConnections/useImportConnectionFile.test.tsx
index d525d6984..1881bebae 100644
--- a/packages/graph-explorer/src/modules/AvailableConnections/useImportConnectionFile.test.tsx
+++ b/packages/graph-explorer/src/modules/AvailableConnections/useImportConnectionFile.test.tsx
@@ -34,7 +34,7 @@ describe("useImportConnectionFile", () => {
id: createNewConfigurationId(),
displayLabel,
connection: {
- url,
+ graphDbUrl: url,
queryEngine: "gremlin" as const,
},
schema: {
@@ -66,7 +66,7 @@ describe("useImportConnectionFile", () => {
c => c.id !== state.activeConfig.id,
);
expect(importedConfig?.displayLabel).toBe(displayLabel);
- expect(importedConfig?.connection?.url).toBe(url);
+ expect(importedConfig?.connection?.graphDbUrl).toBe(url);
expect(importedConfig?.connection?.queryEngine).toBe("gremlin");
const importedSchema = Array.from(schemas.values()).find(
@@ -119,7 +119,7 @@ describe("useImportConnectionFile", () => {
id: state.activeConfig.id,
displayLabel: createRandomName("Config"),
connection: {
- url: createRandomUrlString(),
+ graphDbUrl: createRandomUrlString(),
queryEngine: "gremlin" as const,
},
schema: {
@@ -156,7 +156,7 @@ describe("useImportConnectionFile", () => {
id: createNewConfigurationId(),
displayLabel: createRandomName("Config"),
connection: {
- url: createRandomUrlString(),
+ graphDbUrl: createRandomUrlString(),
queryEngine: "sparql" as const,
},
schema: {
@@ -206,7 +206,7 @@ describe("useImportConnectionFile", () => {
id: createNewConfigurationId(),
displayLabel: createRandomName("Config"),
connection: {
- url: createRandomUrlString(),
+ graphDbUrl: createRandomUrlString(),
queryEngine: "gremlin" as const,
},
schema: {
@@ -248,7 +248,7 @@ describe("useImportConnectionFile", () => {
id: createNewConfigurationId(),
displayLabel: createRandomName("Config"),
connection: {
- url: createRandomUrlString(),
+ graphDbUrl: createRandomUrlString(),
queryEngine: "gremlin" as const,
},
schema: {
@@ -287,7 +287,7 @@ describe("useImportConnectionFile", () => {
id: createNewConfigurationId(),
displayLabel: "Production Database",
connection: {
- url: "https://neptune.example.com:8182",
+ graphDbUrl: "https://neptune.example.com:8182",
queryEngine: "gremlin" as const,
},
schema: {
@@ -357,7 +357,7 @@ describe("useImportConnectionFile", () => {
id: createNewConfigurationId(),
displayLabel: createRandomName("Config"),
connection: {
- url: createRandomUrlString(),
+ graphDbUrl: createRandomUrlString(),
queryEngine: "gremlin" as const,
},
schema: {
@@ -437,7 +437,7 @@ describe("backward compatibility: legacy __matches in exported files", () => {
id: createNewConfigurationId(),
displayLabel: createRandomName("Config"),
connection: {
- url: createRandomUrlString(),
+ graphDbUrl: createRandomUrlString(),
queryEngine: "sparql" as const,
},
schema: {
diff --git a/packages/graph-explorer/src/modules/ConnectionDetail/ConnectionDetail.tsx b/packages/graph-explorer/src/modules/ConnectionDetail/ConnectionDetail.tsx
index 481cfc215..6b5bab56f 100644
--- a/packages/graph-explorer/src/modules/ConnectionDetail/ConnectionDetail.tsx
+++ b/packages/graph-explorer/src/modules/ConnectionDetail/ConnectionDetail.tsx
@@ -83,11 +83,7 @@ function ConnectionDetail({ config }: ConnectionDetailProps) {
const deleteActiveConfig = useDeleteActiveConfiguration();
- const dbUrl = config.connection
- ? config.connection.proxyConnection
- ? config.connection.graphDbUrl
- : config.connection.url
- : LABELS.MISSING_VALUE;
+ const dbUrl = config.connection?.graphDbUrl || LABELS.MISSING_VALUE;
const connectionName = config.displayLabel || config.id;
diff --git a/packages/graph-explorer/src/modules/CreateConnection/CreateConnection.tsx b/packages/graph-explorer/src/modules/CreateConnection/CreateConnection.tsx
index 637a0dc26..f47492cc4 100644
--- a/packages/graph-explorer/src/modules/CreateConnection/CreateConnection.tsx
+++ b/packages/graph-explorer/src/modules/CreateConnection/CreateConnection.tsx
@@ -36,10 +36,8 @@ import {
type ConnectionForm = {
name?: string;
- url?: string;
- queryEngine?: QueryEngine;
- proxyConnection?: boolean;
graphDbUrl?: string;
+ queryEngine?: QueryEngine;
awsAuthEnabled?: boolean;
serviceType?: NeptuneServiceType;
awsRegion?: string;
@@ -65,10 +63,8 @@ export type CreateConnectionProps = {
function mapToConnection(data: Required): ConnectionConfig {
return {
- url: data.url,
- queryEngine: data.queryEngine,
- proxyConnection: data.proxyConnection,
graphDbUrl: data.graphDbUrl,
+ queryEngine: data.queryEngine,
awsAuthEnabled: data.awsAuthEnabled,
serviceType: data.serviceType,
awsRegion: data.awsRegion,
@@ -144,11 +140,10 @@ const CreateConnection = ({
return updated;
});
- const urlChange = initialData?.url !== data.url;
const dbUrlChange = initialData?.graphDbUrl !== data.graphDbUrl;
const typeChange = initialData?.queryEngine !== data.queryEngine;
- if (urlChange || dbUrlChange || typeChange) {
+ if (dbUrlChange || typeChange) {
logger.log(
"Clearing cached schema and previous graph session because connection to database meaningfully changed",
{ original: initialData, updated: data },
@@ -183,8 +178,6 @@ const CreateConnection = ({
name:
initialData?.name ||
`Connection (${formatDate(new Date(), "yyyy-MM-dd HH:mm")})`,
- url: initialData?.url || "",
- proxyConnection: initialData?.proxyConnection || false,
graphDbUrl: initialData?.graphDbUrl || "",
awsAuthEnabled: initialData?.awsAuthEnabled || false,
serviceType: initialData?.serviceType || "neptune-db",
@@ -233,12 +226,7 @@ const CreateConnection = ({
const reset = useResetState();
const onSubmit = () => {
- if (!form.name || !form.url || !form.queryEngine) {
- setError(true);
- return;
- }
-
- if (form.proxyConnection && !form.graphDbUrl) {
+ if (!form.name || !form.graphDbUrl || !form.queryEngine) {
setError(true);
return;
}
@@ -277,63 +265,34 @@ const CreateConnection = ({
- {form.proxyConnection && (
-
-
-
-
- )}
- {form.proxyConnection && (
-
- )}
- {form.proxyConnection && form.awsAuthEnabled && (
+ {form.awsAuthEnabled && (
<>
diff --git a/packages/graph-explorer/src/modules/GraphViewer/ImportGraphButton.test.tsx b/packages/graph-explorer/src/modules/GraphViewer/ImportGraphButton.test.tsx
index e6aeab7ef..17a5004d0 100644
--- a/packages/graph-explorer/src/modules/GraphViewer/ImportGraphButton.test.tsx
+++ b/packages/graph-explorer/src/modules/GraphViewer/ImportGraphButton.test.tsx
@@ -80,30 +80,12 @@ describe("createErrorNotification", () => {
);
});
- it("should show the connection name when a connection using the proxy server is a match", () => {
+ it("should show the connection name when a connection is a match", () => {
const connection = createRandomExportedGraphConnection();
const error = new InvalidConnectionError("test", connection);
const file = createRandomFile();
const allConnections = createRandomAllConnections();
allConnections[0].graphDbUrl = connection.dbUrl;
- allConnections[0].proxyConnection = true;
- allConnections[0].queryEngine = connection.queryEngine;
- const matchingConnectionName = allConnections[0].displayLabel;
-
- const notification = createErrorNotification(error, file, allConnections);
-
- expect(notification).toBe(
- `The graph file requires switching to connection ${matchingConnectionName}.`,
- );
- });
-
- it("should show the connection name when a connection not using the proxy server is a match", () => {
- const connection = createRandomExportedGraphConnection();
- const error = new InvalidConnectionError("test", connection);
- const file = createRandomFile();
- const allConnections = createRandomAllConnections();
- allConnections[0].url = connection.dbUrl;
- allConnections[0].proxyConnection = false;
allConnections[0].queryEngine = connection.queryEngine;
const matchingConnectionName = allConnections[0].displayLabel;
diff --git a/packages/graph-explorer/src/modules/GraphViewer/exportedGraph.test.ts b/packages/graph-explorer/src/modules/GraphViewer/exportedGraph.test.ts
index 207cc74c6..910e83128 100644
--- a/packages/graph-explorer/src/modules/GraphViewer/exportedGraph.test.ts
+++ b/packages/graph-explorer/src/modules/GraphViewer/exportedGraph.test.ts
@@ -136,41 +136,25 @@ describe("createExportedGraph", () => {
});
describe("createExportedConnection", () => {
- it("should map graphDbUrl when using proxy server", () => {
+ it("should map graphDbUrl", () => {
const connection = createRandomConnectionWithId();
- connection.proxyConnection = true;
- connection.graphDbUrl = createRandomUrlString();
const exportedConnection = createExportedConnection(connection);
expect(exportedConnection).toEqual({
- dbUrl: connection.graphDbUrl,
- queryEngine: connection.queryEngine!,
- } satisfies ExportedGraphConnection);
- });
-
- it("should map url when not using proxy server", () => {
- const connection = createRandomConnectionWithId();
- connection.proxyConnection = false;
-
- const exportedConnection = createExportedConnection(connection);
-
- expect(exportedConnection).toEqual({
- dbUrl: connection.url,
+ dbUrl: connection.graphDbUrl.toLowerCase(),
queryEngine: connection.queryEngine!,
} satisfies ExportedGraphConnection);
});
it("should default to gremlin when no query engine is provided", () => {
const connection = createRandomConnectionWithId();
- connection.proxyConnection = true;
- connection.graphDbUrl = createRandomUrlString();
delete connection.queryEngine;
const exportedConnection = createExportedConnection(connection);
expect(exportedConnection).toEqual({
- dbUrl: connection.graphDbUrl,
+ dbUrl: connection.graphDbUrl.toLowerCase(),
queryEngine: "gremlin",
} satisfies ExportedGraphConnection);
});
@@ -351,18 +335,6 @@ describe("isMatchingConnection", () => {
it("should return false when graph db url is different", () => {
const connection = createRandomConnectionWithId();
- connection.proxyConnection = true;
- connection.graphDbUrl = createRandomUrlString();
- const exportedConnection = createRandomExportedGraphConnection();
- exportedConnection.dbUrl = connection.url;
- exportedConnection.queryEngine = connection.queryEngine!;
-
- expect(isMatchingConnection(connection, exportedConnection)).toBeFalsy();
- });
-
- it("should return false when url is different", () => {
- const connection = createRandomConnectionWithId();
- connection.proxyConnection = false;
const exportedConnection = createRandomExportedGraphConnection();
exportedConnection.dbUrl = createRandomUrlString();
exportedConnection.queryEngine = connection.queryEngine!;
diff --git a/packages/graph-explorer/src/modules/GraphViewer/exportedGraph.ts b/packages/graph-explorer/src/modules/GraphViewer/exportedGraph.ts
index 2cb2f3aee..d098ee832 100644
--- a/packages/graph-explorer/src/modules/GraphViewer/exportedGraph.ts
+++ b/packages/graph-explorer/src/modules/GraphViewer/exportedGraph.ts
@@ -62,9 +62,7 @@ export function createExportedGraph(
export function createExportedConnection(
connection: ConnectionConfig,
): ExportedGraphConnection {
- const dbUrl = (
- (connection.proxyConnection ? connection.graphDbUrl : connection.url) ?? ""
- ).toLowerCase();
+ const dbUrl = (connection.graphDbUrl ?? "").toLowerCase();
const queryEngine = connection.queryEngine ?? "gremlin";
return {
diff --git a/packages/graph-explorer/src/utils/constants.ts b/packages/graph-explorer/src/utils/constants.ts
index 357f14c2e..dd3adc2ed 100644
--- a/packages/graph-explorer/src/utils/constants.ts
+++ b/packages/graph-explorer/src/utils/constants.ts
@@ -26,10 +26,7 @@ export const RESERVED_ID_PROPERTY = "~id";
export const RESERVED_TYPES_PROPERTY = "types";
/** The root URL for the app used for reloading fresh. */
-export const RELOAD_URL =
- import.meta.env.BASE_URL.substring(-1) !== "/"
- ? import.meta.env.BASE_URL + "/"
- : import.meta.env.BASE_URL;
+export const RELOAD_URL = ".";
/** Labels used in the UI */
export const LABELS = {
diff --git a/packages/graph-explorer/src/utils/isValidConfigurationFile.test.ts b/packages/graph-explorer/src/utils/isValidConfigurationFile.test.ts
index 80e30b3de..5f99cc158 100644
--- a/packages/graph-explorer/src/utils/isValidConfigurationFile.test.ts
+++ b/packages/graph-explorer/src/utils/isValidConfigurationFile.test.ts
@@ -11,7 +11,7 @@ describe("isValidConfigurationFile", () => {
id: createNewConfigurationId(),
displayLabel: createRandomName("Config"),
connection: {
- url: createRandomUrlString(),
+ graphDbUrl: createRandomUrlString(),
queryEngine: "gremlin" as const,
},
schema: {
@@ -28,7 +28,7 @@ describe("isValidConfigurationFile", () => {
test("should return false when id is missing", () => {
const config = {
connection: {
- url: createRandomUrlString(),
+ graphDbUrl: createRandomUrlString(),
queryEngine: "gremlin" as const,
},
schema: {
@@ -60,7 +60,7 @@ describe("isValidConfigurationFile", () => {
const config = {
id: createNewConfigurationId(),
connection: {
- url: createRandomUrlString(),
+ graphDbUrl: createRandomUrlString(),
queryEngine: "gremlin" as const,
},
};
@@ -68,7 +68,7 @@ describe("isValidConfigurationFile", () => {
expect(isValidConfigurationFile(config)).toBe(false);
});
- test("should return false when connection.url is missing", () => {
+ test("should return false when connection.graphDbUrl is missing", () => {
const config = {
id: createNewConfigurationId(),
connection: {
@@ -89,7 +89,7 @@ describe("isValidConfigurationFile", () => {
const config = {
id: createNewConfigurationId(),
connection: {
- url: createRandomUrlString(),
+ graphDbUrl: createRandomUrlString(),
},
schema: {
totalVertices: 0,
@@ -106,7 +106,7 @@ describe("isValidConfigurationFile", () => {
const config = {
id: createNewConfigurationId(),
connection: {
- url: "not-a-valid-url",
+ graphDbUrl: "not-a-valid-url",
queryEngine: "gremlin" as const,
},
schema: {
@@ -124,7 +124,7 @@ describe("isValidConfigurationFile", () => {
const config = {
id: createNewConfigurationId(),
connection: {
- url: "ftp://example.com",
+ graphDbUrl: "ftp://example.com",
queryEngine: "gremlin" as const,
},
schema: {
@@ -142,7 +142,7 @@ describe("isValidConfigurationFile", () => {
const config = {
id: createNewConfigurationId(),
connection: {
- url: "http://example.com",
+ graphDbUrl: "http://example.com",
queryEngine: "gremlin" as const,
},
schema: {
@@ -160,7 +160,7 @@ describe("isValidConfigurationFile", () => {
const config = {
id: createNewConfigurationId(),
connection: {
- url: "https://example.com",
+ graphDbUrl: "https://example.com",
queryEngine: "gremlin" as const,
},
schema: {
@@ -178,7 +178,7 @@ describe("isValidConfigurationFile", () => {
const config = {
id: createNewConfigurationId(),
connection: {
- url: createRandomUrlString(),
+ graphDbUrl: createRandomUrlString(),
queryEngine: "invalid-engine",
},
schema: {
@@ -196,7 +196,7 @@ describe("isValidConfigurationFile", () => {
const config = {
id: createNewConfigurationId(),
connection: {
- url: createRandomUrlString(),
+ graphDbUrl: createRandomUrlString(),
queryEngine: "gremlin" as const,
},
schema: {
@@ -214,7 +214,7 @@ describe("isValidConfigurationFile", () => {
const config = {
id: createNewConfigurationId(),
connection: {
- url: createRandomUrlString(),
+ graphDbUrl: createRandomUrlString(),
queryEngine: "openCypher" as const,
},
schema: {
@@ -232,7 +232,7 @@ describe("isValidConfigurationFile", () => {
const config = {
id: createNewConfigurationId(),
connection: {
- url: createRandomUrlString(),
+ graphDbUrl: createRandomUrlString(),
queryEngine: "sparql" as const,
},
schema: {
@@ -250,7 +250,7 @@ describe("isValidConfigurationFile", () => {
const config = {
id: createNewConfigurationId(),
connection: {
- url: createRandomUrlString(),
+ graphDbUrl: createRandomUrlString(),
queryEngine: "gremlin" as const,
},
schema: {
@@ -273,7 +273,7 @@ describe("isValidConfigurationFile", () => {
const config = {
id: createNewConfigurationId(),
connection: {
- url: createRandomUrlString(),
+ graphDbUrl: createRandomUrlString(),
queryEngine: "gremlin" as const,
},
schema: {
@@ -295,7 +295,7 @@ describe("isValidConfigurationFile", () => {
const config = {
id: createNewConfigurationId(),
connection: {
- url: createRandomUrlString(),
+ graphDbUrl: createRandomUrlString(),
queryEngine: "gremlin" as const,
},
schema: {
@@ -318,7 +318,7 @@ describe("isValidConfigurationFile", () => {
const config = {
id: createNewConfigurationId(),
connection: {
- url: createRandomUrlString(),
+ graphDbUrl: createRandomUrlString(),
queryEngine: "gremlin" as const,
},
schema: {
@@ -341,7 +341,7 @@ describe("isValidConfigurationFile", () => {
const config = {
id: createNewConfigurationId(),
connection: {
- url: createRandomUrlString(),
+ graphDbUrl: createRandomUrlString(),
queryEngine: "gremlin" as const,
},
schema: {
@@ -364,7 +364,7 @@ describe("isValidConfigurationFile", () => {
id: createNewConfigurationId(),
displayLabel: "Production Database",
connection: {
- url: "https://neptune.example.com:8182",
+ graphDbUrl: "https://neptune.example.com:8182",
queryEngine: "gremlin" as const,
},
schema: {
diff --git a/packages/graph-explorer/src/utils/isValidConfigurationFile.ts b/packages/graph-explorer/src/utils/isValidConfigurationFile.ts
index 863ad2bce..f520a3bde 100644
--- a/packages/graph-explorer/src/utils/isValidConfigurationFile.ts
+++ b/packages/graph-explorer/src/utils/isValidConfigurationFile.ts
@@ -59,9 +59,9 @@ const isValidConfigurationFile = (
}
if (
- !data.connection.url ||
+ !data.connection.graphDbUrl ||
!data.connection.queryEngine ||
- !isValidHttpUrl(data.connection.url) ||
+ !isValidHttpUrl(data.connection.graphDbUrl) ||
!queryEngineOptions.includes(data.connection.queryEngine)
) {
return false;
diff --git a/packages/graph-explorer/src/utils/saveConfigurationToFile.test.ts b/packages/graph-explorer/src/utils/saveConfigurationToFile.test.ts
index e27553d84..ecec64960 100644
--- a/packages/graph-explorer/src/utils/saveConfigurationToFile.test.ts
+++ b/packages/graph-explorer/src/utils/saveConfigurationToFile.test.ts
@@ -56,7 +56,7 @@ describe("saveConfigurationToFile", () => {
const config: ConfigurationContextProps = {
...createRandomRawConfiguration(),
connection: {
- url: "https://example.com",
+ graphDbUrl: "https://example.com",
},
totalVertices: 0,
vertexTypes: [],
@@ -71,14 +71,14 @@ describe("saveConfigurationToFile", () => {
const parsed = JSON.parse(text);
expect(parsed.connection.queryEngine).toBe("gremlin");
- expect(parsed.connection.url).toBe("https://example.com");
+ expect(parsed.connection.graphDbUrl).toBe("https://example.com");
});
it("should preserve existing queryEngine", async () => {
const config: ConfigurationContextProps = {
...createRandomRawConfiguration(),
connection: {
- url: "https://example.com",
+ graphDbUrl: "https://example.com",
queryEngine: "sparql",
},
totalVertices: 0,
diff --git a/packages/graph-explorer/src/utils/testing/createMockExplorer.ts b/packages/graph-explorer/src/utils/testing/createMockExplorer.ts
index ff5be769e..d85d82eda 100644
--- a/packages/graph-explorer/src/utils/testing/createMockExplorer.ts
+++ b/packages/graph-explorer/src/utils/testing/createMockExplorer.ts
@@ -1,5 +1,7 @@
import type { Explorer } from "@/connector";
+import { normalizeConnection } from "@/core/StateProvider/configuration";
+
import { createRandomRawConfiguration } from "./randomData";
export function createMockExplorer(): Explorer {
@@ -8,7 +10,7 @@ export function createMockExplorer(): Explorer {
keywordSearch: vi.fn(),
fetchNeighbors: vi.fn(),
fetchVertexCountsByType: vi.fn(),
- connection: createRandomRawConfiguration().connection!,
+ connection: normalizeConnection(createRandomRawConfiguration().connection!),
fetchSchema: vi.fn(),
edgeDetails: vi.fn(),
vertexDetails: vi.fn(),
diff --git a/packages/graph-explorer/src/utils/testing/randomData.ts b/packages/graph-explorer/src/utils/testing/randomData.ts
index 32396dbcf..a3f0543be 100644
--- a/packages/graph-explorer/src/utils/testing/randomData.ts
+++ b/packages/graph-explorer/src/utils/testing/randomData.ts
@@ -589,7 +589,6 @@ export function createRandomFile(): File {
}
export function createRandomConnectionWithId(): ConnectionWithId {
- const isProxyConnection = createRandomBoolean();
const isIamEnabled = createRandomBoolean();
const fetchTimeoutMs = randomlyUndefined(createRandomInteger());
const nodeExpansionLimit = randomlyUndefined(createRandomInteger());
@@ -599,10 +598,8 @@ export function createRandomConnectionWithId(): ConnectionWithId {
return {
id: createNewConfigurationId(),
displayLabel: createRandomName("displayLabel"),
- url: createRandomUrlString(),
- ...(isProxyConnection && { graphDbUrl: createRandomUrlString() }),
+ graphDbUrl: createRandomUrlString(),
queryEngine,
- proxyConnection: isProxyConnection,
...(isIamEnabled && { awsAuthEnabled: createRandomBoolean() }),
...(isIamEnabled && {
awsRegion: createRandomAwsRegion(),
diff --git a/packages/graph-explorer/vite.config.ts b/packages/graph-explorer/vite.config.ts
index 0fb7f1fca..e86c65d8f 100644
--- a/packages/graph-explorer/vite.config.ts
+++ b/packages/graph-explorer/vite.config.ts
@@ -47,7 +47,7 @@ export default defineConfig(({ mode }) => {
},
},
},
- base: env.GRAPH_EXP_ENV_ROOT_FOLDER,
+ base: "./",
envPrefix: "GRAPH_EXP",
define: {
__GRAPH_EXP_VERSION__: JSON.stringify(process.env.npm_package_version),
diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts
index 78c85d2a0..c2ce756de 100644
--- a/packages/shared/src/types/index.ts
+++ b/packages/shared/src/types/index.ts
@@ -9,23 +9,14 @@ export type NeptuneServiceType = (typeof neptuneServiceTypeOptions)[number];
export type ConnectionConfig = {
/**
- * Base URL to access to the database through HTTPs endpoints
+ * The URL of the graph database endpoint.
*/
- url: string;
+ graphDbUrl: string;
/**
* Choose between gremlin or sparQL engines.
* By default, it uses gremlin
*/
queryEngine?: QueryEngine;
- /**
- * If the service is Neptune,
- * all requests should be sent through the nodejs proxy-server.
- */
- proxyConnection?: boolean;
- /**
- * If it is Neptune, the URL of the database.
- */
- graphDbUrl?: string;
/**
* If it is Neptune, it could need authentication.
*/
@@ -52,3 +43,10 @@ export type ConnectionConfig = {
*/
nodeExpansionLimit?: number;
};
+
+/** Legacy connection config that may still exist in stored data. */
+export type LegacyConnectionConfig = Omit & {
+ graphDbUrl?: string;
+ url?: string;
+ proxyConnection?: boolean;
+};
diff --git a/process-environment.sh b/process-environment.sh
index e6931ec97..eb2c583ae 100644
--- a/process-environment.sh
+++ b/process-environment.sh
@@ -6,10 +6,8 @@ if [ -f "./config.json" ]; then
json=$(cat ./config.json)
- PUBLIC_OR_PROXY_ENDPOINT=$(echo "$json" | grep -o '"PUBLIC_OR_PROXY_ENDPOINT":[^,}]*' | cut -d '"' -f 4)
GRAPH_TYPE=$(echo "$json" | grep -o '"GRAPH_TYPE":[^,}]*' | cut -d '"' -f 4)
SERVICE_TYPE=$(echo "$json" | grep -o '"SERVICE_TYPE":[^,}]*' | cut -d '"' -f 4)
- USING_PROXY_SERVER=$(echo "$json" | grep -o '"USING_PROXY_SERVER":[^,}]*' | cut -d ':' -f 2 | tr -d '[:space:]' | sed 's/"//g')
IAM=$(echo "$json" | grep -o '"IAM":[^,}]*' | cut -d ':' -f 2 | tr -d '[:space:]' | sed 's/"//g')
GRAPH_CONNECTION_URL=$(echo "$json" | grep -o '"GRAPH_CONNECTION_URL":[^,}]*' | cut -d '"' -f 4)
AWS_REGION=$(echo "$json" | grep -o '"AWS_REGION":[^,}]*' | cut -d '"' -f 4)
@@ -18,15 +16,17 @@ if [ -f "./config.json" ]; then
NEPTUNE_NOTEBOOK=$(echo "$json" | grep -o '"NEPTUNE_NOTEBOOK":[^,}]*' | cut -d ':' -f 2 | tr -d '[:space:]' | sed 's/"//g')
fi
-if [ -n "$NEPTUNE_NOTEBOOK" ]; then
- printf '\nNEPTUNE_NOTEBOOK=%s\n' "$NEPTUNE_NOTEBOOK" >> $CONFIGURATION_FOLDER_PATH/.env
- if [ "$NEPTUNE_NOTEBOOK" = "true" ]; then
- # Override Proxy SSL setting if Neptune notebook
- PROXY_SERVER_HTTPS_CONNECTION="false"
- GRAPH_EXP_HTTPS_CONNECTION="false"
+if [ "$NEPTUNE_NOTEBOOK" = "true" ]; then
+ # Force SSL off for Neptune Notebook environments
+ PROXY_SERVER_HTTPS_CONNECTION="false"
+ GRAPH_EXP_HTTPS_CONNECTION="false"
+ # Set port and log style unless explicitly overridden
+ if [ -z "$PROXY_SERVER_HTTP_PORT" ]; then
+ printf '\nPROXY_SERVER_HTTP_PORT=9250\n' >> $CONFIGURATION_FOLDER_PATH/.env
+ fi
+ if [ -z "$LOG_STYLE" ]; then
+ printf '\nLOG_STYLE=cloudwatch\n' >> $CONFIGURATION_FOLDER_PATH/.env
fi
-else
- printf '\nNEPTUNE_NOTEBOOK=false\n' >> $CONFIGURATION_FOLDER_PATH/.env
fi
if [ -n "$PROXY_SERVER_HTTPS_CONNECTION" ]; then
@@ -42,11 +42,11 @@ else
fi
# Update the default connection file with the configuration values
-if [ -n "$PUBLIC_OR_PROXY_ENDPOINT" ]; then
+if [ -n "$GRAPH_CONNECTION_URL" ]; then
# Overwrite existing file with an empty string
echo "" > $CONFIGURATION_FOLDER_PATH/defaultConnection.json
-
- printf '{\n"GRAPH_EXP_PUBLIC_OR_PROXY_ENDPOINT":"%s",\n' "$PUBLIC_OR_PROXY_ENDPOINT" >> $CONFIGURATION_FOLDER_PATH/defaultConnection.json
+
+ printf '{\n"GRAPH_EXP_CONNECTION_URL":"%s",\n' "$GRAPH_CONNECTION_URL" >> $CONFIGURATION_FOLDER_PATH/defaultConnection.json
if [ -n "$SERVICE_TYPE" ]; then
echo "\"GRAPH_EXP_SERVICE_TYPE\":\"${SERVICE_TYPE}\"," >> $CONFIGURATION_FOLDER_PATH/defaultConnection.json
@@ -54,26 +54,19 @@ if [ -n "$PUBLIC_OR_PROXY_ENDPOINT" ]; then
echo "\"GRAPH_EXP_SERVICE_TYPE\":\"neptune-db\"," >> $CONFIGURATION_FOLDER_PATH/defaultConnection.json
fi
- if [ -n "$GRAPH_TYPE" ]; then
+ if [ -n "$GRAPH_TYPE" ]; then
echo "\"GRAPH_EXP_GRAPH_TYPE\":\"${GRAPH_TYPE}\"," >> $CONFIGURATION_FOLDER_PATH/defaultConnection.json
else
if [ "$SERVICE_TYPE" = "neptune-graph" ]; then
echo "\"GRAPH_EXP_GRAPH_TYPE\":\"openCypher\"," >> $CONFIGURATION_FOLDER_PATH/defaultConnection.json
fi
fi
-
- if [ -n "$USING_PROXY_SERVER" ]; then
- echo "\"GRAPH_EXP_USING_PROXY_SERVER\":${USING_PROXY_SERVER}," >> $CONFIGURATION_FOLDER_PATH/defaultConnection.json
- else
- echo "\"GRAPH_EXP_USING_PROXY_SERVER\":false," >> $CONFIGURATION_FOLDER_PATH/defaultConnection.json
- fi
- if [ -n "$IAM" ]; then
+ if [ -n "$IAM" ]; then
echo "\"GRAPH_EXP_IAM\":${IAM}," >> $CONFIGURATION_FOLDER_PATH/defaultConnection.json
- else
+ else
echo "\"GRAPH_EXP_IAM\":false," >> $CONFIGURATION_FOLDER_PATH/defaultConnection.json
fi
- echo "\"GRAPH_EXP_CONNECTION_URL\":\"${GRAPH_CONNECTION_URL}\"," >> $CONFIGURATION_FOLDER_PATH/defaultConnection.json
printf '"GRAPH_EXP_AWS_REGION":"%s"\n}\n' "$AWS_REGION" >> $CONFIGURATION_FOLDER_PATH/defaultConnection.json
fi