diff --git a/.changeset/mrt-command-reorganization.md b/.changeset/mrt-command-reorganization.md new file mode 100644 index 00000000..92d1e697 --- /dev/null +++ b/.changeset/mrt-command-reorganization.md @@ -0,0 +1,5 @@ +--- +'@salesforce/b2c-cli': minor +--- + +Reorganizes MRT commands by scope: project-level commands under `mrt project`, environment-level under `mrt env`, and deployment commands under `mrt bundle`. The `mrt bundle download` command now downloads files by default instead of just printing the URL. diff --git a/.changeset/mrt-complete-coverage.md b/.changeset/mrt-complete-coverage.md new file mode 100644 index 00000000..a674e90e --- /dev/null +++ b/.changeset/mrt-complete-coverage.md @@ -0,0 +1,6 @@ +--- +'@salesforce/b2c-cli': minor +'@salesforce/b2c-tooling-sdk': minor +--- + +Adds complete MRT CLI coverage organized by scope: `mrt project` (CRUD, members, notifications), `mrt env` (CRUD, variables, redirects, access-control, cache invalidation, B2C connections), `mrt bundle` (deploy, list, history, download), `mrt org` (list, B2C instances), and `mrt user` (profile, API key, email preferences). diff --git a/.changeset/mrt-push-to-bundle.md b/.changeset/mrt-push-to-bundle.md new file mode 100644 index 00000000..16ecc30d --- /dev/null +++ b/.changeset/mrt-push-to-bundle.md @@ -0,0 +1,5 @@ +--- +'@salesforce/b2c-cli': minor +--- + +Replaces `mrt push` with `mrt bundle deploy`. The new command supports both pushing local builds and deploying existing bundles by ID. diff --git a/docs/cli/mrt.md b/docs/cli/mrt.md index 879222b1..85783b38 100644 --- a/docs/cli/mrt.md +++ b/docs/cli/mrt.md @@ -1,10 +1,25 @@ --- -description: Commands for managing Managed Runtime projects, pushing bundles, creating environments, and configuring environment variables. +description: Commands for managing Managed Runtime projects, environments, bundles, and deployments. --- # MRT Commands -Commands for managing Managed Runtime (MRT) projects, environments, and bundles. +Commands for managing Managed Runtime (MRT) projects, environments, and bundles for PWA Kit storefronts. + +## Command Overview + +| Topic | Commands | Description | +|-------|----------|-------------| +| `mrt org` | `list`, `b2c` | List organizations and B2C connections | +| `mrt project` | `list`, `create`, `get`, `update`, `delete` | Manage MRT projects | +| `mrt project member` | `list`, `add`, `get`, `update`, `remove` | Manage project members | +| `mrt project notification` | `list`, `create`, `get`, `update`, `delete` | Manage deployment notifications | +| `mrt env` | `list`, `create`, `get`, `update`, `delete`, `invalidate`, `b2c` | Manage environments | +| `mrt env var` | `list`, `set`, `delete` | Manage environment variables | +| `mrt env redirect` | `list`, `create`, `delete`, `clone` | Manage URL redirects | +| `mrt env access-control` | `list` | Manage access control headers | +| `mrt bundle` | `deploy`, `list`, `history`, `download` | Manage bundles and deployments | +| `mrt user` | `profile`, `api-key`, `email-prefs` | Manage user settings | ## Global MRT Flags @@ -27,353 +42,531 @@ MRT commands resolve configuration in the following order of precedence: ## Authentication -MRT commands use API key authentication. The API key is configured in the Managed Runtime dashboard and grants access to specific projects. +MRT commands use API key authentication. The API key is configured in the Managed Runtime dashboard. ### Getting an API Key 1. Log in to the [Managed Runtime dashboard](https://runtime.commercecloud.com/) 2. Navigate to **Account Settings** > **API Keys** 3. Create a new API key or use an existing one -4. The API key grants access to all projects in your organization ### Configuration -Provide the API key via one of these methods (in order of precedence): +Provide the API key via one of these methods: 1. **Command-line flag**: `--api-key your-api-key` 2. **Environment variable**: `export SFCC_MRT_API_KEY=your-api-key` 3. **Mobify config file**: `~/.mobify` with `api_key` field -### Example ~/.mobify File - ```json { "api_key": "your-mrt-api-key" } ``` -### Project Access +--- + +## Organization Commands + +### b2c mrt org list + +List organizations you have access to. + +```bash +b2c mrt org list +b2c mrt org list --json +``` + +### b2c mrt org b2c -Your API key provides access to all projects in your MRT organization. Specify the project using: +Get B2C Commerce instances connected to an organization. -- `--project` flag or `SFCC_MRT_PROJECT` environment variable -- `mrtProject` field in `dw.json` +```bash +b2c mrt org b2c my-organization +b2c mrt org b2c my-organization --json +``` --- -## b2c mrt push +## Project Commands + +### b2c mrt project list + +List MRT projects. -Push a bundle to Managed Runtime. +```bash +b2c mrt project list +b2c mrt project list --limit 10 --offset 0 +b2c mrt project list --json +``` -Creates a bundle from the build directory and uploads it to the specified MRT project. Optionally deploys the bundle to a target environment. +### b2c mrt project create -### Usage +Create a new MRT project. ```bash -b2c mrt push [FLAGS] +b2c mrt project create my-storefront --name "My Storefront" +b2c mrt project create my-storefront --name "My Storefront" --organization my-org ``` -### Flags +### b2c mrt project get -In addition to [global MRT flags](#global-mrt-flags): +Get details of an MRT project. -| Flag | Description | Default | -|------|-------------|---------| -| `--message`, `-m` | Bundle message/description | | -| `--build-dir`, `-b` | Path to the build directory | `build` | -| `--ssr-only` | Glob patterns for server-only files (comma-separated) | `ssr.js,server/**/*` | -| `--ssr-shared` | Glob patterns for shared files (comma-separated) | `static/**/*,client/**/*` | -| `--node-version`, `-n` | Node.js version for SSR runtime | `20.x` | -| `--ssr-param` | SSR parameter in key=value format (can be specified multiple times) | | -| `--json` | Output result as JSON | | +```bash +b2c mrt project get --project my-storefront +b2c mrt project get -p my-storefront --json +``` + +### b2c mrt project update -### Examples +Update an MRT project. ```bash -# Push a bundle to a project -b2c mrt push --project my-storefront +b2c mrt project update --project my-storefront --name "Updated Name" +``` -# Push and deploy to staging -b2c mrt push --project my-storefront --environment staging +### b2c mrt project delete -# Push with a release message -b2c mrt push --project my-storefront --environment production --message "Release v1.0.0" +Delete an MRT project. + +```bash +b2c mrt project delete --project my-storefront +b2c mrt project delete -p my-storefront --force +``` + +--- -# Push from a custom build directory -b2c mrt push --project my-storefront --build-dir ./dist +## Project Member Commands -# Specify Node.js version for SSR -b2c mrt push --project my-storefront --node-version 20.x +### b2c mrt project member list -# Set SSR parameters -b2c mrt push --project my-storefront --ssr-param SSRProxyPath=/api +List members of an MRT project. -# Using environment variables -export SFCC_MRT_API_KEY=your-api-key -export SFCC_MRT_PROJECT=my-storefront -export SFCC_MRT_ENVIRONMENT=staging -b2c mrt push +```bash +b2c mrt project member list --project my-storefront +b2c mrt project member list -p my-storefront --json ``` -### Output +### b2c mrt project member add -On success, the command displays the bundle ID, project, and deployment status: +Add a member to an MRT project. +```bash +b2c mrt project member add user@example.com --project my-storefront --role admin +b2c mrt project member add user@example.com -p my-storefront --role developer ``` -Pushing bundle to my-storefront... -Bundle will be deployed to staging -Bundle #42 pushed to my-storefront and deployed to staging (Release v1.0.0) + +**Roles:** `admin`, `developer`, `viewer` + +### b2c mrt project member get + +Get details of a project member. + +```bash +b2c mrt project member get user@example.com --project my-storefront +``` + +### b2c mrt project member update + +Update a project member's role. + +```bash +b2c mrt project member update user@example.com --project my-storefront --role viewer +``` + +### b2c mrt project member remove + +Remove a member from an MRT project. + +```bash +b2c mrt project member remove user@example.com --project my-storefront +b2c mrt project member remove user@example.com -p my-storefront --force ``` --- -## b2c mrt env create +## Project Notification Commands -Create a new environment (target) in a Managed Runtime project. +Configure email notifications for deployment events. -### Usage +### b2c mrt project notification list + +List notifications for an MRT project. ```bash -b2c mrt env create SLUG [FLAGS] +b2c mrt project notification list --project my-storefront ``` -### Arguments +### b2c mrt project notification create -| Argument | Description | Required | -|----------|-------------|----------| -| `SLUG` | Environment slug/identifier (e.g., staging, production) | Yes | +Create a deployment notification. -### Flags +```bash +# Notify on deployment failures +b2c mrt project notification create -p my-storefront \ + --target staging --target production \ + --recipient ops@example.com \ + --on-failed + +# Notify on all deployment events +b2c mrt project notification create -p my-storefront \ + --target production \ + --recipient team@example.com \ + --on-start --on-success --on-failed +``` -In addition to [global MRT flags](#global-mrt-flags): +### b2c mrt project notification get -| Flag | Description | Default | -|------|-------------|---------| -| `--name`, `-n` | Display name for the environment | **Required** | -| `--region`, `-r` | AWS region for SSR deployment | | -| `--production` | Mark as a production environment | `false` | -| `--hostname` | Hostname pattern for V8 Tag loading | | -| `--external-hostname` | Full external hostname (e.g., www.example.com) | | -| `--external-domain` | External domain for Universal PWA SSR (e.g., example.com) | | -| `--allow-cookies` | Forward HTTP cookies to origin | `false` | -| `--no-allow-cookies` | Disable cookie forwarding | | -| `--enable-source-maps` | Enable source map support in the environment | `false` | -| `--no-enable-source-maps` | Disable source map support | | -| `--json` | Output result as JSON | | +Get details of a notification. + +```bash +b2c mrt project notification get abc-123 --project my-storefront +``` + +### b2c mrt project notification update + +Update a notification. + +```bash +b2c mrt project notification update abc-123 -p my-storefront --on-start --no-on-failed +``` + +### b2c mrt project notification delete + +Delete a notification. + +```bash +b2c mrt project notification delete abc-123 --project my-storefront +b2c mrt project notification delete abc-123 -p my-storefront --force +``` -### Supported Regions +--- -Available AWS regions: `us-east-1`, `us-east-2`, `us-west-1`, `us-west-2`, `ap-south-1`, `ap-south-2`, `ap-northeast-1`, `ap-northeast-2`, `ap-northeast-3`, `ap-southeast-1`, `ap-southeast-2`, `ap-southeast-3`, `ca-central-1`, `eu-central-1`, `eu-central-2`, `eu-west-1`, `eu-west-2`, `eu-west-3`, `eu-north-1`, `eu-south-1`, `il-central-1`, `me-central-1`, `sa-east-1` +## Environment Commands -### Examples +### b2c mrt env list + +List environments in an MRT project. + +```bash +b2c mrt env list --project my-storefront +b2c mrt env list -p my-storefront --json +``` + +### b2c mrt env create + +Create a new environment. ```bash # Create a staging environment b2c mrt env create staging --project my-storefront --name "Staging Environment" -# Create a production environment -b2c mrt env create production --project my-storefront --name "Production" --production - -# Create an environment in a specific region -b2c mrt env create feature-test -p my-storefront -n "Feature Test" --region eu-west-1 +# Create a production environment in a specific region +b2c mrt env create production -p my-storefront --name "Production" \ + --production --region eu-west-1 -# Create with external hostname configuration -b2c mrt env create prod -p my-storefront -n "Production" --production \ +# Create with external hostname +b2c mrt env create prod -p my-storefront --name "Production" \ + --production \ --external-hostname www.example.com \ --external-domain example.com +``` + +**Flags:** +| Flag | Description | +|------|-------------| +| `--name`, `-n` | Display name (required) | +| `--region`, `-r` | AWS region for SSR | +| `--production` | Mark as production | +| `--hostname` | Hostname pattern for V8 Tag | +| `--external-hostname` | Full external hostname | +| `--external-domain` | External domain for SSR | +| `--allow-cookies` | Forward HTTP cookies | +| `--enable-source-maps` | Enable source maps | -# Output as JSON -b2c mrt env create staging -p my-storefront -n "Staging" --json +### b2c mrt env get + +Get environment details. + +```bash +b2c mrt env get --project my-storefront --environment staging +b2c mrt env get -p my-storefront -e production --json ``` -### Output +### b2c mrt env update -On success, displays the created environment details: +Update an environment. +```bash +b2c mrt env update -p my-storefront -e staging --name "Updated Staging" +b2c mrt env update -p my-storefront -e production --allow-cookies ``` -Creating environment "staging" in my-storefront... -Environment created successfully. -Slug: staging -Name: Staging Environment -Project: my-storefront -State: created -Production: No -Region: us-east-1 -Hostname: staging-my-storefront.mobify-storefront.com +### b2c mrt env delete + +Delete an environment. + +```bash +b2c mrt env delete staging --project my-storefront +b2c mrt env delete old-env -p my-storefront --force ``` ---- +### b2c mrt env invalidate -## b2c mrt env delete +Invalidate CDN cache for an environment. -Delete an environment (target) from a Managed Runtime project. +```bash +# Invalidate all cached content +b2c mrt env invalidate -p my-storefront -e production -### Usage +# Invalidate specific paths +b2c mrt env invalidate -p my-storefront -e production --path "/products/*" --path "/categories/*" +``` + +### b2c mrt env b2c + +Get or update B2C Commerce connection for an environment. ```bash -b2c mrt env delete SLUG [FLAGS] +# Get current B2C configuration +b2c mrt env b2c -p my-storefront -e production + +# Set B2C instance connection +b2c mrt env b2c -p my-storefront -e production --instance-id aaaa_prd + +# Set B2C instance with specific sites +b2c mrt env b2c -p my-storefront -e production --instance-id aaaa_prd --sites RefArch,SiteGenesis ``` -### Arguments +--- -| Argument | Description | Required | -|----------|-------------|----------| -| `SLUG` | Environment slug/identifier to delete | Yes | +## Environment Variable Commands -### Flags +### b2c mrt env var list -In addition to [global MRT flags](#global-mrt-flags): +List environment variables. -| Flag | Description | Default | -|------|-------------|---------| -| `--force`, `-f` | Skip confirmation prompt | `false` | -| `--json` | Output result as JSON | | +```bash +b2c mrt env var list --project my-storefront --environment production +b2c mrt env var list -p my-storefront -e staging --json +``` -### Examples +### b2c mrt env var set + +Set environment variables. ```bash -# Delete an environment (with confirmation prompt) -b2c mrt env delete feature-test --project my-storefront +# Set a single variable +b2c mrt env var set MY_VAR=value -p my-storefront -e production + +# Set multiple variables +b2c mrt env var set API_KEY=secret DEBUG=true -p my-storefront -e staging -# Delete without confirmation -b2c mrt env delete old-staging -p my-storefront --force +# Set value with spaces +b2c mrt env var set "MESSAGE=hello world" -p my-storefront -e production ``` -### Notes +### b2c mrt env var delete + +Delete an environment variable. -- The command will prompt for confirmation unless `--force` is used -- Be cautious when deleting production environments +```bash +b2c mrt env var delete MY_VAR -p my-storefront -e production +``` --- -## b2c mrt env var list +## URL Redirect Commands -List environment variables on a Managed Runtime environment. +### b2c mrt env redirect list -### Usage +List URL redirects for an environment. ```bash -b2c mrt env var list [FLAGS] +b2c mrt env redirect list -p my-storefront -e production +b2c mrt env redirect list -p my-storefront -e production --limit 50 ``` -### Flags +### b2c mrt env redirect create -Uses [global MRT flags](#global-mrt-flags). Both `--project` and `--environment` are required. +Create a URL redirect. -| Flag | Description | -|------|-------------| -| `--json` | Output result as JSON | +```bash +b2c mrt env redirect create -p my-storefront -e production \ + --from "/old-path" --to "/new-path" -### Examples +# Permanent redirect (301) +b2c mrt env redirect create -p my-storefront -e production \ + --from "/legacy/*" --to "/modern/$1" --permanent +``` -```bash -# List environment variables -b2c mrt env var list --project acme-storefront --environment production +### b2c mrt env redirect delete -# Short form -b2c mrt env var list -p my-project -e staging +Delete a URL redirect. -# Output as JSON -b2c mrt env var list -p my-project -e production --json +```bash +b2c mrt env redirect delete abc-123 -p my-storefront -e production ``` -### Output +### b2c mrt env redirect clone -Displays a table of environment variables: +Clone redirects from one environment to another. -``` -Listing env vars for my-project/production... -Name Value Status Updated -───────────────────────────────────────────────────────────────────── -API_KEY sk-xxx...xxx Published 12/10/2024, 2:30:00 PM -DEBUG false Published 12/9/2024, 10:15:00 AM -FEATURE_FLAG enabled Pending 12/10/2024, 3:00:00 PM +```bash +b2c mrt env redirect clone -p my-storefront \ + --source staging --target production ``` --- -## b2c mrt env var set +## Access Control Commands -Set environment variables on a Managed Runtime environment. +### b2c mrt env access-control list -### Usage +List access control headers for an environment. ```bash -b2c mrt env var set KEY=value [KEY=value...] [FLAGS] +b2c mrt env access-control list -p my-storefront -e staging +b2c mrt env access-control list -p my-storefront -e staging --json ``` -### Arguments +--- -| Argument | Description | Required | -|----------|-------------|----------| -| `KEY=value` | Environment variable(s) in KEY=value format | Yes | +## Bundle Commands -### Flags +### b2c mrt bundle deploy -Uses [global MRT flags](#global-mrt-flags). Both `--project` and `--environment` are required. +Push a local build or deploy an existing bundle. -| Flag | Description | -|------|-------------| -| `--json` | Output result as JSON | +```bash +# Push local build to project +b2c mrt bundle deploy --project my-storefront + +# Push and deploy to staging +b2c mrt bundle deploy -p my-storefront -e staging -### Examples +# Push with release message +b2c mrt bundle deploy -p my-storefront -e production --message "Release v1.0.0" + +# Push from custom build directory +b2c mrt bundle deploy -p my-storefront --build-dir ./dist + +# Deploy existing bundle by ID +b2c mrt bundle deploy 12345 -p my-storefront -e production +``` + +**Flags:** +| Flag | Description | Default | +|------|-------------|---------| +| `--message`, `-m` | Bundle message/description | | +| `--build-dir`, `-b` | Path to build directory | `build` | +| `--ssr-only` | Server-only file patterns | `ssr.js,ssr.mjs,server/**/*` | +| `--ssr-shared` | Shared file patterns | `static/**/*,client/**/*` | +| `--node-version`, `-n` | Node.js version for SSR | `22.x` | +| `--ssr-param` | SSR parameters (key=value) | | + +### b2c mrt bundle list + +List bundles in a project. ```bash -# Set a single environment variable -b2c mrt env var set MY_VAR=value --project acme-storefront --environment production +b2c mrt bundle list --project my-storefront +b2c mrt bundle list -p my-storefront --limit 10 +b2c mrt bundle list -p my-storefront --json +``` -# Set multiple environment variables -b2c mrt env var set API_KEY=secret DEBUG=true -p my-project -e staging +### b2c mrt bundle history -# Set a value with spaces (use quotes) -b2c mrt env var set "MESSAGE=hello world" -p my-project -e production +View deployment history for an environment. -# Using environment variables for auth -export SFCC_MRT_API_KEY=your-api-key -export SFCC_MRT_PROJECT=my-project -export SFCC_MRT_ENVIRONMENT=staging -b2c mrt env var set MY_VAR=value +```bash +b2c mrt bundle history -p my-storefront -e production +b2c mrt bundle history -p my-storefront -e staging --limit 5 ``` -### Notes +### b2c mrt bundle download + +Download a bundle artifact. -- Variable values are set immediately but may take time to propagate -- Use quotes around values containing spaces -- Multiple variables can be set in a single command +```bash +# Download to current directory +b2c mrt bundle download 12345 -p my-storefront + +# Download to specific path +b2c mrt bundle download 12345 -p my-storefront -o ./artifacts/bundle.tgz + +# Get download URL only +b2c mrt bundle download 12345 -p my-storefront --url-only +``` --- -## b2c mrt env var delete +## User Commands -Delete an environment variable from a Managed Runtime environment. +### b2c mrt user profile -### Usage +View your MRT user profile. ```bash -b2c mrt env var delete KEY [FLAGS] +b2c mrt user profile +b2c mrt user profile --json ``` -### Arguments +### b2c mrt user api-key -| Argument | Description | Required | -|----------|-------------|----------| -| `KEY` | Environment variable name to delete | Yes | +Reset your MRT API key. -### Flags +```bash +b2c mrt user api-key --reset +``` -Uses [global MRT flags](#global-mrt-flags). Both `--project` and `--environment` are required. +### b2c mrt user email-prefs -| Flag | Description | -|------|-------------| -| `--json` | Output result as JSON | +View or update email preferences. + +```bash +# View current preferences +b2c mrt user email-prefs + +# Update preferences +b2c mrt user email-prefs --marketing --no-notifications +``` -### Examples +--- + +## Common Workflows + +### Deploy to Production ```bash -# Delete an environment variable -b2c mrt env var delete MY_VAR --project acme-storefront --environment production +# 1. Push and deploy to staging for testing +b2c mrt bundle deploy -p my-storefront -e staging -m "v1.0.0-rc1" -# Short form -b2c mrt env var delete OLD_API_KEY -p my-project -e staging +# 2. After testing, deploy to production +b2c mrt bundle deploy -p my-storefront -e production -m "v1.0.0" + +# 3. Or deploy an existing bundle +b2c mrt bundle deploy 12345 -p my-storefront -e production +``` + +### Set Up a New Environment + +```bash +# 1. Create the environment +b2c mrt env create qa -p my-storefront --name "QA Environment" --region us-east-1 + +# 2. Configure environment variables +b2c mrt env var set API_URL=https://api.qa.example.com -p my-storefront -e qa + +# 3. Deploy a bundle +b2c mrt bundle deploy -p my-storefront -e qa +``` + +### Invalidate Cache After Content Update + +```bash +# Invalidate specific paths +b2c mrt env invalidate -p my-storefront -e production \ + --path "/products/*" --path "/categories/*" ``` diff --git a/packages/b2c-cli/package.json b/packages/b2c-cli/package.json index d5941a68..729f48e3 100644 --- a/packages/b2c-cli/package.json +++ b/packages/b2c-cli/package.json @@ -102,12 +102,37 @@ "description": "WebDAV file operations (ls, get, put, rm, zip, unzip)" }, "mrt": { - "description": "Manage Managed Runtime projects and deployments", - "subtopics": { - "env-var": { - "description": "Manage environment variables on MRT projects" - } - } + "description": "Manage Managed Runtime (MRT) projects, environments, bundles, and deployments" + }, + "mrt:env": { + "description": "Create, update, delete, and configure MRT environments (targets)" + }, + "mrt:env:var": { + "description": "Set and delete environment variables for MRT environments" + }, + "mrt:env:redirect": { + "description": "Configure URL redirects and clone redirect rules between environments" + }, + "mrt:org": { + "description": "List organizations and view B2C Commerce instance connections" + }, + "mrt:project": { + "description": "Create, update, delete, and configure MRT projects" + }, + "mrt:project:member": { + "description": "Add, remove, and update project member roles and permissions" + }, + "mrt:project:notification": { + "description": "Configure email notifications for deployment events (start, success, failure)" + }, + "mrt:env:access-control": { + "description": "Configure access control headers for environment security" + }, + "mrt:bundle": { + "description": "Push builds, deploy bundles, view deployment history, and download artifacts" + }, + "mrt:user": { + "description": "View profile, reset API keys, and configure email preferences" }, "ods": { "description": "Manage On-Demand Sandboxes" diff --git a/packages/b2c-cli/src/commands/mrt/bundle/deploy.ts b/packages/b2c-cli/src/commands/mrt/bundle/deploy.ts new file mode 100644 index 00000000..761f604a --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/bundle/deploy.ts @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Args, Flags} from '@oclif/core'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import { + pushBundle, + createDeployment, + DEFAULT_SSR_PARAMETERS, + type PushResult, + type CreateDeploymentResult, +} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../i18n/index.js'; + +/** + * Parses SSR parameter flags into a key-value object. + * Accepts format: key=value + */ +function parseSsrParams(params: string[]): Record { + const result: Record = {}; + for (const param of params) { + const eqIndex = param.indexOf('='); + if (eqIndex === -1) { + throw new Error(`Invalid SSR parameter format: "${param}". Expected key=value format.`); + } + const key = param.slice(0, eqIndex); + const value = param.slice(eqIndex + 1); + result[key] = value; + } + return result; +} + +type DeployResult = CreateDeploymentResult | PushResult; + +/** + * Deploy a bundle to Managed Runtime. + * + * Without bundleId: Creates a bundle from the local build directory and uploads it. + * Optionally deploys to a target environment if --environment is specified. + * + * With bundleId: Deploys an existing bundle to the specified environment. + */ +export default class MrtBundleDeploy extends MrtCommand { + static args = { + bundleId: Args.integer({ + description: 'Bundle ID to deploy (omit to push local build)', + required: false, + }), + }; + + static description = t( + 'commands.mrt.bundle.deploy.description', + 'Push a local build or deploy an existing bundle to Managed Runtime', + ); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> --project my-storefront', + '<%= config.bin %> <%= command.id %> --project my-storefront --environment staging', + '<%= config.bin %> <%= command.id %> --project my-storefront --environment production --message "Release v1.0.0"', + '<%= config.bin %> <%= command.id %> --project my-storefront --build-dir ./dist', + '<%= config.bin %> <%= command.id %> --project my-storefront --node-version 20.x', + '<%= config.bin %> <%= command.id %> --project my-storefront --ssr-param SSRProxyPath=/api', + '<%= config.bin %> <%= command.id %> 12345 --project my-storefront --environment staging', + ]; + + static flags = { + ...MrtCommand.baseFlags, + message: Flags.string({ + char: 'm', + description: 'Bundle message/description (only for local builds)', + }), + 'build-dir': Flags.string({ + char: 'b', + description: 'Path to the build directory (only for local builds)', + default: 'build', + }), + 'ssr-only': Flags.string({ + description: 'Glob patterns for server-only files (comma-separated, only for local builds)', + default: 'ssr.js,ssr.mjs,server/**/*', + }), + 'ssr-shared': Flags.string({ + description: 'Glob patterns for shared files (comma-separated, only for local builds)', + default: 'static/**/*,client/**/*', + }), + 'node-version': Flags.string({ + char: 'n', + description: `Node.js version for SSR runtime (default: ${DEFAULT_SSR_PARAMETERS.SSRFunctionNodeVersion}, only for local builds)`, + }), + 'ssr-param': Flags.string({ + description: 'SSR parameter in key=value format (can be specified multiple times, only for local builds)', + multiple: true, + default: [], + }), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {bundleId} = this.args; + + if (bundleId !== undefined) { + return this.deployExistingBundle(bundleId); + } + return this.pushLocalBuild(); + } + + /** + * Deploy an existing bundle to an environment. + */ + private async deployExistingBundle(bundleId: number): Promise { + const {mrtProject: project, mrtEnvironment: environment} = this.resolvedConfig.values; + + if (!project) { + this.error( + 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', + ); + } + if (!environment) { + this.error( + 'MRT environment is required when deploying an existing bundle. Provide --environment flag, set SFCC_MRT_ENVIRONMENT, or set mrtEnvironment in dw.json.', + ); + } + + this.log( + t('commands.mrt.bundle.deploy.deploying', 'Deploying bundle {{bundleId}} to {{project}}/{{environment}}...', { + bundleId, + project, + environment, + }), + ); + + try { + const result = await createDeployment( + { + projectSlug: project, + targetSlug: environment, + bundleId, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.log( + t( + 'commands.mrt.bundle.deploy.deploySuccess', + 'Deployment started. Bundle {{bundleId}} is being deployed to {{environment}}.', + { + bundleId, + environment, + }, + ), + ); + this.log( + t( + 'commands.mrt.bundle.deploy.note', + 'Note: Deployments are asynchronous. Use "b2c mrt env get" or the Runtime Admin dashboard to check status.', + ), + ); + } + + return result; + } catch (error) { + if (error instanceof Error) { + this.error( + t('commands.mrt.bundle.deploy.deployFailed', 'Failed to create deployment: {{message}}', { + message: error.message, + }), + ); + } + throw error; + } + } + + /** + * Push a local build to create a new bundle. + */ + private async pushLocalBuild(): Promise { + const {mrtProject: project, mrtEnvironment: target} = this.resolvedConfig.values; + const {message} = this.flags; + + if (!project) { + this.error( + 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', + ); + } + + const buildDir = this.flags['build-dir']; + const ssrOnly = this.flags['ssr-only'].split(',').map((s) => s.trim()); + const ssrShared = this.flags['ssr-shared'].split(',').map((s) => s.trim()); + + // Build SSR parameters from flags + const ssrParameters: Record = parseSsrParams(this.flags['ssr-param']); + + // --node-version is a convenience flag for SSRFunctionNodeVersion + if (this.flags['node-version']) { + ssrParameters.SSRFunctionNodeVersion = this.flags['node-version']; + } + + this.log(t('commands.mrt.bundle.deploy.pushing', 'Pushing bundle to {{project}}...', {project})); + + if (target) { + this.log( + t('commands.mrt.bundle.deploy.willDeploy', 'Bundle will be deployed to {{environment}}', {environment: target}), + ); + } + + try { + const result = await pushBundle( + { + projectSlug: project, + target, + message, + buildDirectory: buildDir, + ssrOnly, + ssrShared, + ssrParameters, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + // Consolidated success output + const deployedMsg = result.deployed && result.target ? ` and deployed to ${result.target}` : ''; + this.log( + t( + 'commands.mrt.bundle.deploy.pushSuccess', + 'Bundle #{{bundleId}} pushed to {{project}}{{deployed}} ({{message}})', + { + bundleId: String(result.bundleId), + project: result.projectSlug, + deployed: deployedMsg, + message: result.message, + }, + ), + ); + + return result; + } catch (error) { + if (error instanceof Error) { + this.error(t('commands.mrt.bundle.deploy.pushFailed', 'Push failed: {{message}}', {message: error.message})); + } + throw error; + } + } +} diff --git a/packages/b2c-cli/src/commands/mrt/bundle/download.ts b/packages/b2c-cli/src/commands/mrt/bundle/download.ts new file mode 100644 index 00000000..f89cccbc --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/bundle/download.ts @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {createWriteStream, mkdirSync} from 'node:fs'; +import {dirname, resolve} from 'node:path'; +import {pipeline} from 'node:stream/promises'; +import {Args, Flags} from '@oclif/core'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {downloadBundle, type DownloadBundleResult} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../i18n/index.js'; + +type BundleDownloadResult = DownloadBundleResult & {filePath?: string}; + +/** + * Download a bundle artifact from Managed Runtime. + */ +export default class MrtBundleDownload extends MrtCommand { + static args = { + bundleId: Args.integer({ + description: 'Bundle ID to download', + required: true, + }), + }; + + static description = t('commands.mrt.bundle.download.description', 'Download a Managed Runtime bundle artifact'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> 12345 --project my-storefront', + '<%= config.bin %> <%= command.id %> 12345 -p my-storefront -o ./artifacts/my-bundle.tgz', + '<%= config.bin %> <%= command.id %> 12345 -p my-storefront --url-only', + '<%= config.bin %> <%= command.id %> 12345 -p my-storefront --json', + ]; + + static flags = { + ...MrtCommand.baseFlags, + output: Flags.string({ + char: 'o', + description: 'Output file path (default: bundle-{bundleId}.tgz)', + }), + 'url-only': Flags.boolean({ + description: 'Only output the download URL without downloading', + default: false, + }), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {bundleId} = this.args; + const {mrtProject: project} = this.resolvedConfig.values; + + if (!project) { + this.error( + 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', + ); + } + + const urlOnly = this.flags['url-only']; + + this.log( + t('commands.mrt.bundle.download.fetching', 'Fetching download URL for bundle {{bundleId}} from {{project}}...', { + bundleId, + project, + }), + ); + + try { + const result = await downloadBundle( + { + projectSlug: project, + bundleId, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + // If url-only flag or JSON mode, just return the URL + if (urlOnly) { + if (!this.jsonEnabled()) { + this.log( + t('commands.mrt.bundle.download.urlOnly', 'Download URL (valid for 1 hour):\n{{downloadUrl}}', { + downloadUrl: result.downloadUrl, + }), + ); + } + return result; + } + + // Download the file + const outputPath = this.flags.output ?? `bundle-${bundleId}.tgz`; + const absolutePath = resolve(outputPath); + + this.log( + t('commands.mrt.bundle.download.downloading', 'Downloading bundle {{bundleId}} to {{filePath}}...', { + bundleId, + filePath: outputPath, + }), + ); + + const response = await fetch(result.downloadUrl); + if (!response.ok) { + this.error( + t('commands.mrt.bundle.download.httpError', 'Failed to download bundle: HTTP {{status}}', { + status: response.status, + }), + ); + } + + if (!response.body) { + this.error(t('commands.mrt.bundle.download.noBody', 'Failed to download bundle: empty response')); + } + + // Ensure directory exists + const dir = dirname(absolutePath); + if (dir !== '.') { + mkdirSync(dir, {recursive: true}); + } + + // Stream the response to file + const fileStream = createWriteStream(absolutePath); + await pipeline(response.body, fileStream); + + if (!this.jsonEnabled()) { + this.log( + t('commands.mrt.bundle.download.success', 'Bundle {{bundleId}} downloaded to {{filePath}}', { + bundleId, + filePath: outputPath, + }), + ); + } + + return {...result, filePath: absolutePath}; + } catch (error) { + if (error instanceof Error) { + this.error( + t('commands.mrt.bundle.download.failed', 'Failed to download bundle: {{message}}', {message: error.message}), + ); + } + throw error; + } + } +} diff --git a/packages/b2c-cli/src/commands/mrt/bundle/history.ts b/packages/b2c-cli/src/commands/mrt/bundle/history.ts new file mode 100644 index 00000000..a21a892b --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/bundle/history.ts @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Flags} from '@oclif/core'; +import {MrtCommand, createTable, type ColumnDef} from '@salesforce/b2c-tooling-sdk/cli'; +import { + listDeployments, + type ListDeploymentsResult, + type MrtDeployment, +} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../i18n/index.js'; + +const COLUMNS: Record> = { + bundleId: { + header: 'Bundle ID', + get: (deploy) => deploy.bundle?.id?.toString() ?? '-', + }, + bundleMessage: { + header: 'Message', + get: (deploy) => deploy.bundle?.message ?? '-', + }, + status: { + header: 'Status', + get: (deploy) => deploy.status ?? '-', + }, + type: { + header: 'Type', + get: (deploy) => deploy.deploy_type ?? '-', + }, + user: { + header: 'User', + get: (deploy) => deploy.user ?? '-', + }, + created: { + header: 'Created', + get: (deploy) => (deploy.created_at ? new Date(deploy.created_at).toLocaleString() : '-'), + }, +}; + +const DEFAULT_COLUMNS = ['bundleId', 'bundleMessage', 'status', 'type', 'created']; + +/** + * List deployment history for an MRT environment. + */ +export default class MrtBundleHistory extends MrtCommand { + static description = t( + 'commands.mrt.bundle.history.description', + 'List deployment history for a Managed Runtime environment', + ); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> --project my-storefront --environment staging', + '<%= config.bin %> <%= command.id %> -p my-storefront -e production --limit 5', + '<%= config.bin %> <%= command.id %> -p my-storefront -e staging --json', + ]; + + static flags = { + ...MrtCommand.baseFlags, + limit: Flags.integer({ + description: 'Maximum number of results to return', + }), + offset: Flags.integer({ + description: 'Offset for pagination', + }), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {mrtProject: project, mrtEnvironment: environment} = this.resolvedConfig.values; + + if (!project) { + this.error( + 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', + ); + } + if (!environment) { + this.error( + 'MRT environment is required. Provide --environment flag, set SFCC_MRT_ENVIRONMENT, or set mrtEnvironment in dw.json.', + ); + } + + const {limit, offset} = this.flags; + + this.log( + t('commands.mrt.bundle.history.fetching', 'Fetching deployment history for {{project}}/{{environment}}...', { + project, + environment, + }), + ); + + const result = await listDeployments( + { + projectSlug: project, + targetSlug: environment, + limit, + offset, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + if (result.deployments.length === 0) { + this.log(t('commands.mrt.bundle.history.empty', 'No deployments found.')); + } else { + this.log(t('commands.mrt.bundle.history.count', 'Found {{count}} deployment(s):', {count: result.count})); + createTable(COLUMNS).render(result.deployments, DEFAULT_COLUMNS); + } + } + + return result; + } +} diff --git a/packages/b2c-cli/src/commands/mrt/bundle/list.ts b/packages/b2c-cli/src/commands/mrt/bundle/list.ts new file mode 100644 index 00000000..1e4a2bf0 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/bundle/list.ts @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Flags} from '@oclif/core'; +import {MrtCommand, createTable, type ColumnDef} from '@salesforce/b2c-tooling-sdk/cli'; +import {listBundles, type ListBundlesResult, type MrtBundle} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../i18n/index.js'; + +const COLUMNS: Record> = { + id: { + header: 'ID', + get: (bundle) => bundle.id?.toString() ?? '-', + }, + message: { + header: 'Message', + get: (bundle) => bundle.message ?? '-', + }, + status: { + header: 'Status', + get: (bundle) => bundle.status?.toString() ?? '-', + }, + user: { + header: 'User', + get: (bundle) => bundle.user ?? '-', + }, + created: { + header: 'Created', + get: (bundle) => (bundle.created_at ? new Date(bundle.created_at).toLocaleString() : '-'), + }, +}; + +const DEFAULT_COLUMNS = ['id', 'message', 'status', 'user', 'created']; + +/** + * List bundles for an MRT project. + */ +export default class MrtBundleList extends MrtCommand { + static description = t('commands.mrt.bundle.list.description', 'List bundles for a Managed Runtime project'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> --project my-storefront', + '<%= config.bin %> <%= command.id %> -p my-storefront --limit 10', + '<%= config.bin %> <%= command.id %> -p my-storefront --json', + ]; + + static flags = { + ...MrtCommand.baseFlags, + limit: Flags.integer({ + description: 'Maximum number of results to return', + }), + offset: Flags.integer({ + description: 'Offset for pagination', + }), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {mrtProject: project} = this.resolvedConfig.values; + + if (!project) { + this.error( + 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', + ); + } + + const {limit, offset} = this.flags; + + this.log(t('commands.mrt.bundle.list.fetching', 'Fetching bundles for {{project}}...', {project})); + + const result = await listBundles( + { + projectSlug: project, + limit, + offset, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + if (result.bundles.length === 0) { + this.log(t('commands.mrt.bundle.list.empty', 'No bundles found.')); + } else { + this.log(t('commands.mrt.bundle.list.count', 'Found {{count}} bundle(s):', {count: result.count})); + createTable(COLUMNS).render(result.bundles, DEFAULT_COLUMNS); + } + } + + return result; + } +} diff --git a/packages/b2c-cli/src/commands/mrt/env/access-control/list.ts b/packages/b2c-cli/src/commands/mrt/env/access-control/list.ts new file mode 100644 index 00000000..95607d78 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/env/access-control/list.ts @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Flags} from '@oclif/core'; +import {MrtCommand, createTable, type ColumnDef} from '@salesforce/b2c-tooling-sdk/cli'; +import { + listAccessControlHeaders, + type ListAccessControlHeadersResult, + type MrtAccessControlHeader, +} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../../i18n/index.js'; + +const COLUMNS: Record> = { + id: { + header: 'ID', + get: (h) => h.id ?? '-', + }, + value: { + header: 'Value', + get: (h) => h.value ?? '-', + }, + status: { + header: 'Status', + get: (h) => h.publishing_status_description ?? '-', + }, + created: { + header: 'Created', + get: (h) => (h.created_at ? new Date(h.created_at).toLocaleString() : '-'), + }, +}; + +const DEFAULT_COLUMNS = ['id', 'value', 'status', 'created']; + +/** + * List access control headers for an MRT environment. + */ +export default class MrtAccessControlList extends MrtCommand { + static description = t( + 'commands.mrt.access-control.list.description', + 'List access control headers for a Managed Runtime environment', + ); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> --project my-storefront --environment production', + '<%= config.bin %> <%= command.id %> -p my-storefront -e production --json', + ]; + + static flags = { + ...MrtCommand.baseFlags, + limit: Flags.integer({ + description: 'Maximum number of results to return', + }), + offset: Flags.integer({ + description: 'Offset for pagination', + }), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {mrtProject: project, mrtEnvironment: environment} = this.resolvedConfig.values; + + if (!project) { + this.error( + 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', + ); + } + if (!environment) { + this.error( + 'MRT environment is required. Provide --environment flag, set SFCC_MRT_ENVIRONMENT, or set mrtEnvironment in dw.json.', + ); + } + + const {limit, offset} = this.flags; + + this.log( + t( + 'commands.mrt.access-control.list.fetching', + 'Fetching access control headers for {{project}}/{{environment}}...', + { + project, + environment, + }, + ), + ); + + const result = await listAccessControlHeaders( + { + projectSlug: project, + targetSlug: environment, + limit, + offset, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + if (result.headers.length === 0) { + this.log(t('commands.mrt.access-control.list.empty', 'No access control headers found.')); + } else { + this.log( + t('commands.mrt.access-control.list.count', 'Found {{count}} access control header(s):', { + count: result.count, + }), + ); + createTable(COLUMNS).render(result.headers, DEFAULT_COLUMNS); + } + } + + return result; + } +} diff --git a/packages/b2c-cli/src/commands/mrt/env/b2c.ts b/packages/b2c-cli/src/commands/mrt/env/b2c.ts new file mode 100644 index 00000000..f06db9cb --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/env/b2c.ts @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Flags} from '@oclif/core'; +import {MrtCommand, createTable, type ColumnDef} from '@salesforce/b2c-tooling-sdk/cli'; +import { + getB2CTargetInfo, + setB2CTargetInfo, + updateB2CTargetInfo, + type B2CTargetInfo, +} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../i18n/index.js'; + +type InfoEntry = {field: string; value: string}; + +const COLUMNS: Record> = { + field: { + header: 'Field', + get: (e) => e.field, + }, + value: { + header: 'Value', + get: (e) => e.value, + }, +}; + +const DEFAULT_COLUMNS = ['field', 'value']; + +/** + * Get or update B2C Commerce info for a target/environment. + */ +export default class MrtB2CTargetInfo extends MrtCommand { + static description = t( + 'commands.mrt.b2c.target-info.description', + 'Get or update B2C Commerce connection for a target/environment', + ); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> -p my-storefront -e production', + '<%= config.bin %> <%= command.id %> -p my-storefront -e production --instance-id aaaa_prd', + '<%= config.bin %> <%= command.id %> -p my-storefront -e production --instance-id aaaa_prd --sites RefArch,SiteGenesis', + '<%= config.bin %> <%= command.id %> -p my-storefront -e production --json', + ]; + + static flags = { + ...MrtCommand.baseFlags, + 'instance-id': Flags.string({ + description: 'B2C Commerce instance ID to connect', + }), + sites: Flags.string({ + description: 'Comma-separated list of site IDs to connect', + }), + 'clear-sites': Flags.boolean({ + description: 'Clear the sites list', + default: false, + }), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {mrtProject: project, mrtEnvironment: environment} = this.resolvedConfig.values; + + if (!project) { + this.error( + 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', + ); + } + if (!environment) { + this.error( + 'MRT environment is required. Provide --environment flag, set SFCC_MRT_ENVIRONMENT, or set mrtEnvironment in dw.json.', + ); + } + + const instanceId = this.flags['instance-id']; + const sitesStr = this.flags.sites; + const clearSites = this.flags['clear-sites']; + + // If instance-id is provided, set or update the target info + if (instanceId) { + this.log( + t('commands.mrt.b2c.target-info.setting', 'Setting B2C info for {{project}}/{{environment}}...', { + project, + environment, + }), + ); + + const sites = clearSites ? null : sitesStr ? sitesStr.split(',').map((s) => s.trim()) : undefined; + + const info = await setB2CTargetInfo( + { + projectSlug: project, + targetSlug: environment, + instanceId, + sites, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.log(t('commands.mrt.b2c.target-info.updated', 'B2C target info updated successfully.')); + this.displayInfo(info); + } + + return info; + } + + // If only sites or clear-sites is provided, update + if (sitesStr !== undefined || clearSites) { + this.log( + t('commands.mrt.b2c.target-info.updating', 'Updating B2C info for {{project}}/{{environment}}...', { + project, + environment, + }), + ); + + const sites = clearSites ? null : sitesStr ? sitesStr.split(',').map((s) => s.trim()) : undefined; + + const info = await updateB2CTargetInfo( + { + projectSlug: project, + targetSlug: environment, + sites, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.log(t('commands.mrt.b2c.target-info.updated', 'B2C target info updated successfully.')); + this.displayInfo(info); + } + + return info; + } + + // Otherwise, get the current info + this.log( + t('commands.mrt.b2c.target-info.fetching', 'Fetching B2C info for {{project}}/{{environment}}...', { + project, + environment, + }), + ); + + const info = await getB2CTargetInfo( + { + projectSlug: project, + targetSlug: environment, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.displayInfo(info); + } + + return info; + } + + private displayInfo(info: B2CTargetInfo): void { + const entries: InfoEntry[] = [ + {field: 'Instance ID', value: info.instance_id ?? '-'}, + {field: 'Sites', value: info.sites && info.sites.length > 0 ? info.sites.join(', ') : 'None'}, + ]; + createTable(COLUMNS).render(entries, DEFAULT_COLUMNS); + } +} diff --git a/packages/b2c-cli/src/commands/mrt/env/get.ts b/packages/b2c-cli/src/commands/mrt/env/get.ts new file mode 100644 index 00000000..4d4a011e --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/env/get.ts @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {ux} from '@oclif/core'; +import cliui from 'cliui'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {getEnv, type MrtEnvironment} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../i18n/index.js'; + +/** + * Print environment details in a formatted table. + */ +function printEnvDetails(env: MrtEnvironment, project: string): void { + const ui = cliui({width: process.stdout.columns || 80}); + const labelWidth = 18; + + ui.div(''); + ui.div({text: 'Slug:', width: labelWidth}, {text: env.slug ?? ''}); + ui.div({text: 'Name:', width: labelWidth}, {text: env.name}); + ui.div({text: 'Project:', width: labelWidth}, {text: project}); + ui.div({text: 'State:', width: labelWidth}, {text: env.state ?? 'unknown'}); + ui.div({text: 'Production:', width: labelWidth}, {text: env.is_production ? 'Yes' : 'No'}); + + if (env.ssr_region) { + ui.div({text: 'Region:', width: labelWidth}, {text: env.ssr_region}); + } + + if (env.hostname) { + ui.div({text: 'Hostname:', width: labelWidth}, {text: env.hostname}); + } + + if (env.ssr_external_hostname) { + ui.div({text: 'External Host:', width: labelWidth}, {text: env.ssr_external_hostname}); + } + + if (env.ssr_external_domain) { + ui.div({text: 'External Domain:', width: labelWidth}, {text: env.ssr_external_domain}); + } + + if (env.allow_cookies) { + ui.div({text: 'Allow Cookies:', width: labelWidth}, {text: 'Yes'}); + } + + if (env.enable_source_maps) { + ui.div({text: 'Source Maps:', width: labelWidth}, {text: 'Yes'}); + } + + if (env.log_level) { + ui.div({text: 'Log Level:', width: labelWidth}, {text: env.log_level}); + } + + if (env.ssr_proxy_configs && env.ssr_proxy_configs.length > 0) { + ui.div({text: 'Proxies:', width: labelWidth}, {text: ''}); + for (const proxy of env.ssr_proxy_configs) { + const proxyPath = (proxy as {path?: string}).path ?? ''; + ui.div({text: '', width: labelWidth}, {text: ` ${proxyPath} → ${proxy.host}`}); + } + } + + ux.stdout(ui.toString()); +} + +/** + * Get details of a Managed Runtime environment. + */ +export default class MrtEnvGet extends MrtCommand { + static description = t('commands.mrt.env.get.description', 'Get details of a Managed Runtime environment'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> --project my-storefront --environment staging', + '<%= config.bin %> <%= command.id %> -p my-storefront -e production --json', + ]; + + static flags = { + ...MrtCommand.baseFlags, + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {mrtProject: project, mrtEnvironment: environment} = this.resolvedConfig.values; + + if (!project) { + this.error( + 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', + ); + } + if (!environment) { + this.error( + 'MRT environment is required. Provide --environment flag, set SFCC_MRT_ENVIRONMENT, or set mrtEnvironment in dw.json.', + ); + } + + this.log( + t('commands.mrt.env.get.fetching', 'Fetching environment {{environment}} in {{project}}...', { + project, + environment, + }), + ); + + try { + const result = await getEnv( + { + projectSlug: project, + slug: environment, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + printEnvDetails(result, project); + } + + return result; + } catch (error) { + if (error instanceof Error) { + this.error( + t('commands.mrt.env.get.failed', 'Failed to get environment: {{message}}', {message: error.message}), + ); + } + throw error; + } + } +} diff --git a/packages/b2c-cli/src/commands/mrt/env/invalidate.ts b/packages/b2c-cli/src/commands/mrt/env/invalidate.ts new file mode 100644 index 00000000..9e65fff6 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/env/invalidate.ts @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Flags} from '@oclif/core'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {invalidateCache, type InvalidateCacheResult} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../i18n/index.js'; + +/** + * Invalidate cached objects in the CDN. + */ +export default class MrtCacheInvalidate extends MrtCommand { + static description = t( + 'commands.mrt.cache.invalidate.description', + 'Invalidate cached objects in the CDN for a Managed Runtime environment', + ); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> --project my-storefront --environment production --pattern "/*"', + '<%= config.bin %> <%= command.id %> -p my-storefront -e production --pattern "/products/*"', + '<%= config.bin %> <%= command.id %> -p my-storefront -e production --pattern "/category/shoes"', + ]; + + static flags = { + ...MrtCommand.baseFlags, + pattern: Flags.string({ + description: 'Path pattern to invalidate (must start with /, use /* for all)', + required: true, + }), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {mrtProject: project, mrtEnvironment: environment} = this.resolvedConfig.values; + + if (!project) { + this.error( + 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', + ); + } + if (!environment) { + this.error( + 'MRT environment is required. Provide --environment flag, set SFCC_MRT_ENVIRONMENT, or set mrtEnvironment in dw.json.', + ); + } + + const {pattern} = this.flags; + + // Validate pattern starts with / + if (!pattern.startsWith('/')) { + this.error('Pattern must start with a forward slash (/).'); + } + + this.log( + t('commands.mrt.cache.invalidate.invalidating', 'Invalidating cache for pattern "{{pattern}}"...', {pattern}), + ); + + try { + const result = await invalidateCache( + { + projectSlug: project, + targetSlug: environment, + pattern, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.log(t('commands.mrt.cache.invalidate.success', '{{result}}', {result: result.result})); + this.log( + t( + 'commands.mrt.cache.invalidate.note', + 'Note: Cache invalidations are asynchronous and usually complete within two minutes.', + ), + ); + } + + return result; + } catch (error) { + if (error instanceof Error) { + this.error( + t('commands.mrt.cache.invalidate.failed', 'Failed to invalidate cache: {{message}}', { + message: error.message, + }), + ); + } + throw error; + } + } +} diff --git a/packages/b2c-cli/src/commands/mrt/env/list.ts b/packages/b2c-cli/src/commands/mrt/env/list.ts new file mode 100644 index 00000000..0d6b1098 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/env/list.ts @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {MrtCommand, createTable, type ColumnDef} from '@salesforce/b2c-tooling-sdk/cli'; +import {listEnvs, type ListEnvsResult, type MrtEnvironment} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../i18n/index.js'; + +const COLUMNS: Record> = { + name: { + header: 'Name', + get: (env) => env.name, + }, + slug: { + header: 'Slug', + get: (env) => env.slug ?? '', + }, + state: { + header: 'State', + get: (env) => env.state ?? '-', + }, + region: { + header: 'Region', + get: (env) => env.ssr_region ?? '-', + }, + production: { + header: 'Prod', + get: (env) => (env.is_production ? 'Yes' : 'No'), + }, +}; + +const DEFAULT_COLUMNS = ['name', 'slug', 'state', 'region', 'production']; + +/** + * List environments (targets) for an MRT project. + */ +export default class MrtEnvList extends MrtCommand { + static description = t('commands.mrt.env.list.description', 'List Managed Runtime environments'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> --project my-storefront', + '<%= config.bin %> <%= command.id %> -p my-storefront --json', + ]; + + static flags = { + ...MrtCommand.baseFlags, + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {mrtProject: project} = this.resolvedConfig.values; + + if (!project) { + this.error( + 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', + ); + } + + this.log(t('commands.mrt.env.list.fetching', 'Fetching environments for {{project}}...', {project})); + + const result = await listEnvs( + { + projectSlug: project, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + if (result.environments.length === 0) { + this.log(t('commands.mrt.env.list.empty', 'No environments found.')); + } else { + this.log( + t('commands.mrt.env.list.count', 'Found {{count}} environment(s):', {count: result.environments.length}), + ); + createTable(COLUMNS).render(result.environments, DEFAULT_COLUMNS); + } + } + + return result; + } +} diff --git a/packages/b2c-cli/src/commands/mrt/env/redirect/clone.ts b/packages/b2c-cli/src/commands/mrt/env/redirect/clone.ts new file mode 100644 index 00000000..f8e4e747 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/env/redirect/clone.ts @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import * as readline from 'node:readline'; +import {Flags} from '@oclif/core'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {cloneRedirects, type CloneRedirectsResult} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../../i18n/index.js'; + +/** + * Prompt for confirmation. + */ +async function confirm(message: string): Promise { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + return new Promise((resolve) => { + rl.question(`${message} (y/N): `, (answer) => { + rl.close(); + resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'); + }); + }); +} + +/** + * Clone redirects from one environment to another. + */ +export default class MrtRedirectClone extends MrtCommand { + static description = t('commands.mrt.redirect.clone.description', 'Clone redirects from one environment to another'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> --project my-storefront --from staging --to production', + '<%= config.bin %> <%= command.id %> -p my-storefront --from staging --to production --force', + ]; + + static flags = { + ...MrtCommand.baseFlags, + from: Flags.string({ + description: 'Source environment to clone redirects from', + required: true, + }), + to: Flags.string({ + description: 'Destination environment to clone redirects to', + required: true, + }), + force: Flags.boolean({ + char: 'f', + description: 'Skip confirmation prompt', + default: false, + }), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {mrtProject: project} = this.resolvedConfig.values; + + if (!project) { + this.error( + 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', + ); + } + + const {from: fromTarget, to: toTarget, force} = this.flags; + + // Confirm clone unless --force is specified + if (!force && !this.jsonEnabled()) { + const confirmed = await confirm( + t( + 'commands.mrt.redirect.clone.confirm', + 'WARNING: This will REPLACE all redirects in {{toTarget}} with redirects from {{fromTarget}}. Continue?', + {fromTarget, toTarget}, + ), + ); + if (!confirmed) { + this.log(t('commands.mrt.redirect.clone.cancelled', 'Clone cancelled.')); + return {count: 0, redirects: []}; + } + } + + this.log( + t('commands.mrt.redirect.clone.cloning', 'Cloning redirects from {{fromTarget}} to {{toTarget}}...', { + fromTarget, + toTarget, + }), + ); + + try { + const result = await cloneRedirects( + { + projectSlug: project, + fromTargetSlug: fromTarget, + toTargetSlug: toTarget, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.log( + t( + 'commands.mrt.redirect.clone.success', + 'Cloned {{count}} redirect(s) from {{fromTarget}} to {{toTarget}}.', + { + count: result.count, + fromTarget, + toTarget, + }, + ), + ); + } + + return result; + } catch (error) { + if (error instanceof Error) { + this.error( + t('commands.mrt.redirect.clone.failed', 'Failed to clone redirects: {{message}}', {message: error.message}), + ); + } + throw error; + } + } +} diff --git a/packages/b2c-cli/src/commands/mrt/env/redirect/create.ts b/packages/b2c-cli/src/commands/mrt/env/redirect/create.ts new file mode 100644 index 00000000..de2b2fe6 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/env/redirect/create.ts @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Flags} from '@oclif/core'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import { + createRedirect, + type MrtRedirect, + type RedirectHttpStatusCode, +} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../../i18n/index.js'; + +/** + * Create a redirect for an MRT environment. + */ +export default class MrtRedirectCreate extends MrtCommand { + static description = t( + 'commands.mrt.redirect.create.description', + 'Create a redirect for a Managed Runtime environment', + ); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> --project my-storefront --environment staging --from /old --to /new', + '<%= config.bin %> <%= command.id %> -p my-storefront -e staging --from /sale --to /summer-sale --status 302', + '<%= config.bin %> <%= command.id %> -p my-storefront -e staging --from "/a/*" --to /b --forward-wildcard', + ]; + + static flags = { + ...MrtCommand.baseFlags, + from: Flags.string({ + description: 'Source path to redirect from', + required: true, + }), + to: Flags.string({ + description: 'Destination URL to redirect to', + required: true, + }), + status: Flags.integer({ + description: 'HTTP status code (301 or 302)', + options: ['301', '302'], + default: 301, + }), + 'forward-querystring': Flags.boolean({ + description: 'Forward query string parameters', + default: false, + }), + 'forward-wildcard': Flags.boolean({ + description: 'Forward wildcard path portion', + default: false, + }), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {mrtProject: project, mrtEnvironment: environment} = this.resolvedConfig.values; + + if (!project) { + this.error( + 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', + ); + } + if (!environment) { + this.error( + 'MRT environment is required. Provide --environment flag, set SFCC_MRT_ENVIRONMENT, or set mrtEnvironment in dw.json.', + ); + } + + const { + from: fromPath, + to: toUrl, + status, + 'forward-querystring': forwardQs, + 'forward-wildcard': forwardWildcard, + } = this.flags; + + this.log( + t('commands.mrt.redirect.create.creating', 'Creating redirect {{from}} -> {{to}}...', { + from: fromPath, + to: toUrl, + }), + ); + + try { + const result = await createRedirect( + { + projectSlug: project, + targetSlug: environment, + fromPath, + toUrl, + httpStatusCode: status as RedirectHttpStatusCode, + forwardQuerystring: forwardQs || undefined, + forwardWildcard: forwardWildcard || undefined, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.log( + t('commands.mrt.redirect.create.success', 'Redirect created: {{from}} -> {{to}}', { + from: fromPath, + to: toUrl, + }), + ); + this.log( + t( + 'commands.mrt.redirect.create.note', + 'Note: Changes may take up to 20 minutes to take effect on your site.', + ), + ); + } + + return result; + } catch (error) { + if (error instanceof Error) { + this.error( + t('commands.mrt.redirect.create.failed', 'Failed to create redirect: {{message}}', {message: error.message}), + ); + } + throw error; + } + } +} diff --git a/packages/b2c-cli/src/commands/mrt/env/redirect/delete.ts b/packages/b2c-cli/src/commands/mrt/env/redirect/delete.ts new file mode 100644 index 00000000..02e18d4d --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/env/redirect/delete.ts @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import * as readline from 'node:readline'; +import {Args, Flags} from '@oclif/core'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {deleteRedirect} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../../i18n/index.js'; + +/** + * Prompt for confirmation. + */ +async function confirm(message: string): Promise { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + return new Promise((resolve) => { + rl.question(`${message} (y/N): `, (answer) => { + rl.close(); + resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'); + }); + }); +} + +/** + * Delete a redirect from an MRT environment. + */ +export default class MrtRedirectDelete extends MrtCommand { + static args = { + fromPath: Args.string({ + description: 'Source path of the redirect to delete', + required: true, + }), + }; + + static description = t( + 'commands.mrt.redirect.delete.description', + 'Delete a redirect from a Managed Runtime environment', + ); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> /old-page --project my-storefront --environment staging', + '<%= config.bin %> <%= command.id %> /old-page -p my-storefront -e staging --force', + ]; + + static flags = { + ...MrtCommand.baseFlags, + force: Flags.boolean({ + char: 'f', + description: 'Skip confirmation prompt', + default: false, + }), + }; + + async run(): Promise<{fromPath: string; deleted: boolean}> { + this.requireMrtCredentials(); + + const {fromPath} = this.args; + const {mrtProject: project, mrtEnvironment: environment} = this.resolvedConfig.values; + + if (!project) { + this.error( + 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', + ); + } + if (!environment) { + this.error( + 'MRT environment is required. Provide --environment flag, set SFCC_MRT_ENVIRONMENT, or set mrtEnvironment in dw.json.', + ); + } + + const {force} = this.flags; + + // Confirm deletion unless --force is specified + if (!force && !this.jsonEnabled()) { + const confirmed = await confirm( + t('commands.mrt.redirect.delete.confirm', 'Are you sure you want to delete redirect "{{fromPath}}"?', { + fromPath, + }), + ); + if (!confirmed) { + this.log(t('commands.mrt.redirect.delete.cancelled', 'Deletion cancelled.')); + return {fromPath, deleted: false}; + } + } + + this.log(t('commands.mrt.redirect.delete.deleting', 'Deleting redirect {{fromPath}}...', {fromPath})); + + try { + await deleteRedirect( + { + projectSlug: project, + targetSlug: environment, + fromPath, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.log(t('commands.mrt.redirect.delete.success', 'Redirect {{fromPath}} deleted.', {fromPath})); + } + + return {fromPath, deleted: true}; + } catch (error) { + if (error instanceof Error) { + this.error( + t('commands.mrt.redirect.delete.failed', 'Failed to delete redirect: {{message}}', {message: error.message}), + ); + } + throw error; + } + } +} diff --git a/packages/b2c-cli/src/commands/mrt/env/redirect/list.ts b/packages/b2c-cli/src/commands/mrt/env/redirect/list.ts new file mode 100644 index 00000000..d269acbe --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/env/redirect/list.ts @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Flags} from '@oclif/core'; +import {MrtCommand, createTable, type ColumnDef} from '@salesforce/b2c-tooling-sdk/cli'; +import {listRedirects, type ListRedirectsResult, type MrtRedirect} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../../i18n/index.js'; + +const COLUMNS: Record> = { + fromPath: { + header: 'From', + get: (r) => r.from_path ?? '-', + }, + toUrl: { + header: 'To', + get: (r) => r.to_url ?? '-', + }, + status: { + header: 'HTTP', + get: (r) => r.http_status_code?.toString() ?? '301', + }, + publishingStatus: { + header: 'Status', + get: (r) => r.publishing_status ?? '-', + }, + forwardQs: { + header: 'Fwd QS', + get: (r) => (r.forward_querystring ? 'Yes' : 'No'), + }, +}; + +const DEFAULT_COLUMNS = ['fromPath', 'toUrl', 'status', 'publishingStatus']; + +/** + * List redirects for an MRT environment. + */ +export default class MrtRedirectList extends MrtCommand { + static description = t('commands.mrt.redirect.list.description', 'List redirects for a Managed Runtime environment'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> --project my-storefront --environment staging', + '<%= config.bin %> <%= command.id %> -p my-storefront -e staging --search "/old"', + '<%= config.bin %> <%= command.id %> -p my-storefront -e staging --json', + ]; + + static flags = { + ...MrtCommand.baseFlags, + limit: Flags.integer({ + description: 'Maximum number of results to return', + }), + offset: Flags.integer({ + description: 'Offset for pagination', + }), + search: Flags.string({ + description: 'Search term for filtering', + }), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {mrtProject: project, mrtEnvironment: environment} = this.resolvedConfig.values; + + if (!project) { + this.error( + 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', + ); + } + if (!environment) { + this.error( + 'MRT environment is required. Provide --environment flag, set SFCC_MRT_ENVIRONMENT, or set mrtEnvironment in dw.json.', + ); + } + + const {limit, offset, search} = this.flags; + + this.log( + t('commands.mrt.redirect.list.fetching', 'Fetching redirects for {{project}}/{{environment}}...', { + project, + environment, + }), + ); + + const result = await listRedirects( + { + projectSlug: project, + targetSlug: environment, + limit, + offset, + search, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + if (result.redirects.length === 0) { + this.log(t('commands.mrt.redirect.list.empty', 'No redirects found.')); + } else { + this.log(t('commands.mrt.redirect.list.count', 'Found {{count}} redirect(s):', {count: result.count})); + createTable(COLUMNS).render(result.redirects, DEFAULT_COLUMNS); + } + } + + return result; + } +} diff --git a/packages/b2c-cli/src/commands/mrt/env/update.ts b/packages/b2c-cli/src/commands/mrt/env/update.ts new file mode 100644 index 00000000..3a31b819 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/env/update.ts @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Flags, ux} from '@oclif/core'; +import cliui from 'cliui'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {updateEnv, type MrtEnvironmentUpdate} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../i18n/index.js'; + +/** + * Proxy configuration for SSR. + */ +interface SsrProxyConfig { + host: string; + path: string; +} + +/** + * Parse a proxy string in format "path=host" into a proxy config object. + */ +function parseProxyString(proxyStr: string): SsrProxyConfig { + const eqIndex = proxyStr.indexOf('='); + if (eqIndex === -1) { + throw new Error(`Invalid proxy format: "${proxyStr}". Expected format: path=host.example.com`); + } + + const path = proxyStr.slice(0, eqIndex); + const host = proxyStr.slice(eqIndex + 1); + + if (!path) { + throw new Error(`Invalid proxy format: "${proxyStr}". Path cannot be empty.`); + } + + if (!host) { + throw new Error(`Invalid proxy format: "${proxyStr}". Host cannot be empty.`); + } + + return {path, host}; +} + +/** + * Print environment details in a formatted table. + */ +function printEnvDetails(env: MrtEnvironmentUpdate, project: string): void { + const ui = cliui({width: process.stdout.columns || 80}); + const labelWidth = 18; + + ui.div(''); + ui.div({text: 'Slug:', width: labelWidth}, {text: env.slug ?? ''}); + ui.div({text: 'Name:', width: labelWidth}, {text: env.name ?? ''}); + ui.div({text: 'Project:', width: labelWidth}, {text: project}); + ui.div({text: 'State:', width: labelWidth}, {text: env.state ?? 'unknown'}); + ui.div({text: 'Production:', width: labelWidth}, {text: env.is_production ? 'Yes' : 'No'}); + + if (env.ssr_region) { + ui.div({text: 'Region:', width: labelWidth}, {text: env.ssr_region}); + } + + if (env.hostname) { + ui.div({text: 'Hostname:', width: labelWidth}, {text: env.hostname}); + } + + if (env.ssr_external_hostname) { + ui.div({text: 'External Host:', width: labelWidth}, {text: env.ssr_external_hostname}); + } + + if (env.ssr_external_domain) { + ui.div({text: 'External Domain:', width: labelWidth}, {text: env.ssr_external_domain}); + } + + if (env.allow_cookies) { + ui.div({text: 'Allow Cookies:', width: labelWidth}, {text: 'Yes'}); + } + + if (env.enable_source_maps) { + ui.div({text: 'Source Maps:', width: labelWidth}, {text: 'Yes'}); + } + + if (env.log_level) { + ui.div({text: 'Log Level:', width: labelWidth}, {text: env.log_level}); + } + + if (env.ssr_proxy_configs && env.ssr_proxy_configs.length > 0) { + ui.div({text: 'Proxies:', width: labelWidth}, {text: ''}); + for (const proxy of env.ssr_proxy_configs) { + const proxyPath = (proxy as {path?: string}).path ?? ''; + ui.div({text: '', width: labelWidth}, {text: ` ${proxyPath} → ${proxy.host}`}); + } + } + + ux.stdout(ui.toString()); +} + +/** + * Valid log levels for MRT environments. + */ +const LOG_LEVELS = ['DEBUG', 'INFO', 'WARN', 'ERROR', 'TRACE', 'FATAL'] as const; + +type LogLevel = (typeof LOG_LEVELS)[number]; + +/** + * Update a Managed Runtime environment. + */ +export default class MrtEnvUpdate extends MrtCommand { + static description = t('commands.mrt.env.update.description', 'Update a Managed Runtime environment'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> --project my-storefront --environment staging --name "New Name"', + '<%= config.bin %> <%= command.id %> -p my-storefront -e staging --enable-source-maps', + '<%= config.bin %> <%= command.id %> -p my-storefront -e staging --production', + '<%= config.bin %> <%= command.id %> -p my-storefront -e staging --proxy api=api.example.com', + ]; + + static flags = { + ...MrtCommand.baseFlags, + name: Flags.string({ + char: 'n', + description: 'Display name for the environment', + }), + production: Flags.boolean({ + description: 'Mark as a production environment', + allowNo: true, + }), + hostname: Flags.string({ + description: 'Hostname pattern for V8 Tag loading (use empty string to clear)', + }), + 'external-hostname': Flags.string({ + description: 'Full external hostname (use empty string to clear)', + }), + 'external-domain': Flags.string({ + description: 'External domain for Universal PWA SSR (use empty string to clear)', + }), + 'allow-cookies': Flags.boolean({ + description: 'Forward HTTP cookies to origin', + allowNo: true, + }), + 'enable-source-maps': Flags.boolean({ + description: 'Enable source map support in the environment', + allowNo: true, + }), + 'log-level': Flags.string({ + description: 'Log level for the environment', + options: LOG_LEVELS as unknown as string[], + }), + 'whitelisted-ips': Flags.string({ + description: 'IP whitelist (CIDR blocks, space-separated; use empty string to clear)', + }), + proxy: Flags.string({ + description: 'Proxy configuration in format path=host (can be specified multiple times)', + multiple: true, + }), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {mrtProject: project, mrtEnvironment: environment} = this.resolvedConfig.values; + + if (!project) { + this.error( + 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', + ); + } + if (!environment) { + this.error( + 'MRT environment is required. Provide --environment flag, set SFCC_MRT_ENVIRONMENT, or set mrtEnvironment in dw.json.', + ); + } + + const { + name, + production, + hostname, + 'external-hostname': externalHostname, + 'external-domain': externalDomain, + 'allow-cookies': allowCookies, + 'enable-source-maps': enableSourceMaps, + 'log-level': logLevel, + 'whitelisted-ips': whitelistedIps, + proxy: proxyStrings, + } = this.flags; + + // Parse proxy configurations + const proxyConfigs = proxyStrings?.map((p) => parseProxyString(p)); + + this.log( + t('commands.mrt.env.update.updating', 'Updating environment {{environment}} in {{project}}...', { + project, + environment, + }), + ); + + try { + const result = await updateEnv( + { + projectSlug: project, + slug: environment, + name, + isProduction: production, + hostname: hostname === '' ? null : hostname, + externalHostname: externalHostname === '' ? null : externalHostname, + externalDomain: externalDomain === '' ? null : externalDomain, + allowCookies, + enableSourceMaps, + logLevel: logLevel as LogLevel | undefined, + whitelistedIps: whitelistedIps === '' ? null : whitelistedIps, + proxyConfigs, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.log(t('commands.mrt.env.update.success', 'Environment updated successfully.')); + this.log( + t( + 'commands.mrt.env.update.note', + 'Note: SSR-related changes will trigger an automatic redeployment of the current bundle.', + ), + ); + printEnvDetails(result, project); + } + + return result; + } catch (error) { + if (error instanceof Error) { + this.error( + t('commands.mrt.env.update.failed', 'Failed to update environment: {{message}}', {message: error.message}), + ); + } + throw error; + } + } +} diff --git a/packages/b2c-cli/src/commands/mrt/org/b2c.ts b/packages/b2c-cli/src/commands/mrt/org/b2c.ts new file mode 100644 index 00000000..decc7097 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/org/b2c.ts @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Args} from '@oclif/core'; +import {MrtCommand, createTable, type ColumnDef} from '@salesforce/b2c-tooling-sdk/cli'; +import {getB2COrgInfo, type B2COrgInfo} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../i18n/index.js'; + +type InfoEntry = {field: string; value: string}; + +const COLUMNS: Record> = { + field: { + header: 'Field', + get: (e) => e.field, + }, + value: { + header: 'Value', + get: (e) => e.value, + }, +}; + +const DEFAULT_COLUMNS = ['field', 'value']; + +/** + * Get B2C Commerce info for an organization. + */ +export default class MrtB2COrgInfo extends MrtCommand { + static args = { + organization: Args.string({ + description: 'Organization slug', + required: true, + }), + }; + + static description = t( + 'commands.mrt.b2c.org-info.description', + 'Get B2C Commerce instances connected to an organization', + ); + + static enableJsonFlag = true; + + static examples = ['<%= config.bin %> <%= command.id %> my-org', '<%= config.bin %> <%= command.id %> my-org --json']; + + static flags = { + ...MrtCommand.baseFlags, + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {organization} = this.args; + + this.log( + t('commands.mrt.b2c.org-info.fetching', 'Fetching B2C info for organization {{org}}...', {org: organization}), + ); + + const info = await getB2COrgInfo( + { + organizationSlug: organization, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + const entries: InfoEntry[] = [ + {field: 'B2C Customer', value: info.is_b2c_customer ? 'Yes' : 'No'}, + {field: 'Instances', value: info.instances.length > 0 ? info.instances.join(', ') : 'None'}, + ]; + createTable(COLUMNS).render(entries, DEFAULT_COLUMNS); + } + + return info; + } +} diff --git a/packages/b2c-cli/src/commands/mrt/org/list.ts b/packages/b2c-cli/src/commands/mrt/org/list.ts new file mode 100644 index 00000000..4142c76f --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/org/list.ts @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Flags} from '@oclif/core'; +import {MrtCommand, createTable, type ColumnDef} from '@salesforce/b2c-tooling-sdk/cli'; +import { + listOrganizations, + type ListOrganizationsResult, + type MrtOrganization, +} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../i18n/index.js'; + +const COLUMNS: Record> = { + name: { + header: 'Name', + get: (org) => org.name, + }, + slug: { + header: 'Slug', + get: (org) => org.slug, + }, + status: { + header: 'Status', + get: (org) => org.deletion_status ?? 'active', + }, + created: { + header: 'Created', + get: (org) => (org.created_at ? new Date(org.created_at).toLocaleDateString() : '-'), + }, +}; + +const DEFAULT_COLUMNS = ['name', 'slug', 'status', 'created']; + +/** + * List MRT organizations accessible to the authenticated user. + */ +export default class MrtOrgList extends MrtCommand { + static description = t('commands.mrt.org.list.description', 'List Managed Runtime organizations'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %>', + '<%= config.bin %> <%= command.id %> --limit 10', + '<%= config.bin %> <%= command.id %> --json', + ]; + + static flags = { + ...MrtCommand.baseFlags, + limit: Flags.integer({ + description: 'Maximum number of results to return', + }), + offset: Flags.integer({ + description: 'Offset for pagination', + }), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {limit, offset} = this.flags; + + this.log(t('commands.mrt.org.list.fetching', 'Fetching organizations...')); + + const result = await listOrganizations( + { + limit, + offset, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + if (result.organizations.length === 0) { + this.log(t('commands.mrt.org.list.empty', 'No organizations found.')); + } else { + this.log(t('commands.mrt.org.list.count', 'Found {{count}} organization(s):', {count: result.count})); + createTable(COLUMNS).render(result.organizations, DEFAULT_COLUMNS); + } + } + + return result; + } +} diff --git a/packages/b2c-cli/src/commands/mrt/project/create.ts b/packages/b2c-cli/src/commands/mrt/project/create.ts new file mode 100644 index 00000000..078cc656 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/project/create.ts @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Args, Flags, ux} from '@oclif/core'; +import cliui from 'cliui'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {createProject, type MrtProject} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../i18n/index.js'; + +/** + * Valid AWS regions for MRT projects. + */ +const SSR_REGIONS = [ + 'us-east-1', + 'us-east-2', + 'us-west-1', + 'us-west-2', + 'ap-south-1', + 'ap-south-2', + 'ap-northeast-2', + 'ap-southeast-1', + 'ap-southeast-2', + 'ap-southeast-3', + 'ap-northeast-1', + 'ap-northeast-3', + 'ca-central-1', + 'eu-central-1', + 'eu-central-2', + 'eu-west-1', + 'eu-west-2', + 'eu-west-3', + 'eu-north-1', + 'eu-south-1', + 'il-central-1', + 'me-central-1', + 'sa-east-1', +] as const; + +type SsrRegion = (typeof SSR_REGIONS)[number]; + +/** + * Print project details in a formatted display. + */ +function printProjectDetails(project: MrtProject): void { + const ui = cliui({width: process.stdout.columns || 80}); + const labelWidth = 16; + + ui.div(''); + ui.div({text: 'Name:', width: labelWidth}, {text: project.name}); + ui.div({text: 'Slug:', width: labelWidth}, {text: project.slug ?? ''}); + ui.div({text: 'Organization:', width: labelWidth}, {text: project.organization}); + + if (project.ssr_region) { + ui.div({text: 'Region:', width: labelWidth}, {text: project.ssr_region}); + } + + if (project.url) { + ui.div({text: 'URL:', width: labelWidth}, {text: project.url}); + } + + if (project.created_at) { + ui.div({text: 'Created:', width: labelWidth}, {text: new Date(project.created_at).toLocaleString()}); + } + + ux.stdout(ui.toString()); +} + +/** + * Create a new MRT project. + */ +export default class MrtProjectCreate extends MrtCommand { + static args = { + name: Args.string({ + description: 'Project name', + required: true, + }), + }; + + static description = t('commands.mrt.project.create.description', 'Create a new Managed Runtime project'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> "My Storefront" --organization my-org', + '<%= config.bin %> <%= command.id %> "My Storefront" -o my-org --slug my-storefront', + '<%= config.bin %> <%= command.id %> "My Storefront" -o my-org --region us-east-1', + ]; + + static flags = { + ...MrtCommand.baseFlags, + organization: Flags.string({ + char: 'o', + description: 'Organization slug to create the project in', + required: true, + }), + slug: Flags.string({ + char: 's', + description: 'Project slug (auto-generated if not provided)', + }), + url: Flags.string({ + description: 'Project URL', + }), + region: Flags.string({ + char: 'r', + description: 'Default AWS region for new environments', + options: SSR_REGIONS as unknown as string[], + }), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {name} = this.args; + const {organization, slug, url, region} = this.flags; + + this.log( + t('commands.mrt.project.create.creating', 'Creating project "{{name}}" in {{organization}}...', { + name, + organization, + }), + ); + + try { + const result = await createProject( + { + name, + organization, + slug, + url, + ssrRegion: region as SsrRegion | undefined, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (this.jsonEnabled()) { + return result; + } + + this.log(t('commands.mrt.project.create.success', 'Project created successfully.')); + printProjectDetails(result); + + return result; + } catch (error) { + if (error instanceof Error) { + this.error( + t('commands.mrt.project.create.failed', 'Failed to create project: {{message}}', {message: error.message}), + ); + } + throw error; + } + } +} diff --git a/packages/b2c-cli/src/commands/mrt/project/delete.ts b/packages/b2c-cli/src/commands/mrt/project/delete.ts new file mode 100644 index 00000000..78ab702e --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/project/delete.ts @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import * as readline from 'node:readline'; +import {Args, Flags} from '@oclif/core'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {deleteProject} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../i18n/index.js'; + +/** + * Simple confirmation prompt. + */ +async function confirm(message: string): Promise { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stderr, + }); + + return new Promise((resolve) => { + rl.question(`${message} `, (answer) => { + rl.close(); + resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'); + }); + }); +} + +/** + * Delete result for JSON output. + */ +interface DeleteResult { + slug: string; + deleted: boolean; +} + +/** + * Delete an MRT project. + */ +export default class MrtProjectDelete extends MrtCommand { + static args = { + slug: Args.string({ + description: 'Project slug', + required: true, + }), + }; + + static description = t('commands.mrt.project.delete.description', 'Delete a Managed Runtime project'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> my-old-project', + '<%= config.bin %> <%= command.id %> my-old-project --force', + ]; + + static flags = { + ...MrtCommand.baseFlags, + force: Flags.boolean({ + char: 'f', + description: 'Skip confirmation prompt', + default: false, + }), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {slug} = this.args; + const {force} = this.flags; + + // Confirm deletion unless --force is specified + if (!force && !this.jsonEnabled()) { + const confirmed = await confirm( + t('commands.mrt.project.delete.confirm', 'Are you sure you want to delete project "{{slug}}"? (y/n)', {slug}), + ); + + if (!confirmed) { + this.log(t('commands.mrt.project.delete.cancelled', 'Deletion cancelled.')); + return {slug, deleted: false}; + } + } + + this.log(t('commands.mrt.project.delete.deleting', 'Deleting project "{{slug}}"...', {slug})); + + try { + await deleteProject( + { + projectSlug: slug, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.log(t('commands.mrt.project.delete.success', 'Project "{{slug}}" deleted successfully.', {slug})); + } + + return {slug, deleted: true}; + } catch (error) { + if (error instanceof Error) { + this.error( + t('commands.mrt.project.delete.failed', 'Failed to delete project: {{message}}', {message: error.message}), + ); + } + throw error; + } + } +} diff --git a/packages/b2c-cli/src/commands/mrt/project/get.ts b/packages/b2c-cli/src/commands/mrt/project/get.ts new file mode 100644 index 00000000..ac161b5e --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/project/get.ts @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Args, ux} from '@oclif/core'; +import cliui from 'cliui'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {getProject, type MrtProjectUpdate} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../i18n/index.js'; + +/** + * Print project details in a formatted display. + */ +function printProjectDetails(project: MrtProjectUpdate): void { + const ui = cliui({width: process.stdout.columns || 80}); + const labelWidth = 16; + + ui.div(''); + ui.div({text: 'Name:', width: labelWidth}, {text: project.name}); + ui.div({text: 'Slug:', width: labelWidth}, {text: project.slug ?? ''}); + ui.div({text: 'Organization:', width: labelWidth}, {text: project.organization ?? ''}); + ui.div({text: 'Type:', width: labelWidth}, {text: project.project_type ?? '-'}); + ui.div({text: 'Status:', width: labelWidth}, {text: project.deletion_status ?? 'active'}); + + if (project.ssr_region) { + ui.div({text: 'Region:', width: labelWidth}, {text: project.ssr_region}); + } + + if (project.url) { + ui.div({text: 'URL:', width: labelWidth}, {text: project.url}); + } + + if (project.created_at) { + ui.div({text: 'Created:', width: labelWidth}, {text: new Date(project.created_at).toLocaleString()}); + } + + if (project.updated_at) { + ui.div({text: 'Updated:', width: labelWidth}, {text: new Date(project.updated_at).toLocaleString()}); + } + + ux.stdout(ui.toString()); +} + +/** + * Get details of an MRT project. + */ +export default class MrtProjectGet extends MrtCommand { + static args = { + slug: Args.string({ + description: 'Project slug', + required: true, + }), + }; + + static description = t('commands.mrt.project.get.description', 'Get details of a Managed Runtime project'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> my-storefront', + '<%= config.bin %> <%= command.id %> my-storefront --json', + ]; + + static flags = { + ...MrtCommand.baseFlags, + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {slug} = this.args; + + this.log(t('commands.mrt.project.get.fetching', 'Fetching project "{{slug}}"...', {slug})); + + try { + const result = await getProject( + { + projectSlug: slug, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (this.jsonEnabled()) { + return result; + } + + printProjectDetails(result); + + return result; + } catch (error) { + if (error instanceof Error) { + this.error( + t('commands.mrt.project.get.failed', 'Failed to get project: {{message}}', {message: error.message}), + ); + } + throw error; + } + } +} diff --git a/packages/b2c-cli/src/commands/mrt/project/list.ts b/packages/b2c-cli/src/commands/mrt/project/list.ts new file mode 100644 index 00000000..c6d88c8c --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/project/list.ts @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Flags} from '@oclif/core'; +import {MrtCommand, createTable, type ColumnDef} from '@salesforce/b2c-tooling-sdk/cli'; +import {listProjects, type ListProjectsResult, type MrtProject} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../i18n/index.js'; + +const COLUMNS: Record> = { + name: { + header: 'Name', + get: (proj) => proj.name, + }, + slug: { + header: 'Slug', + get: (proj) => proj.slug ?? '', + }, + organization: { + header: 'Organization', + get: (proj) => proj.organization, + }, + region: { + header: 'Region', + get: (proj) => proj.ssr_region ?? '-', + }, + created: { + header: 'Created', + get: (proj) => (proj.created_at ? new Date(proj.created_at).toLocaleDateString() : '-'), + }, +}; + +const DEFAULT_COLUMNS = ['name', 'slug', 'organization', 'region']; + +/** + * List MRT projects accessible to the authenticated user. + */ +export default class MrtProjectList extends MrtCommand { + static description = t('commands.mrt.project.list.description', 'List Managed Runtime projects'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %>', + '<%= config.bin %> <%= command.id %> --organization my-org', + '<%= config.bin %> <%= command.id %> --limit 10', + '<%= config.bin %> <%= command.id %> --json', + ]; + + static flags = { + ...MrtCommand.baseFlags, + organization: Flags.string({ + char: 'o', + description: 'Filter by organization slug', + }), + limit: Flags.integer({ + description: 'Maximum number of results to return', + }), + offset: Flags.integer({ + description: 'Offset for pagination', + }), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {organization, limit, offset} = this.flags; + + this.log(t('commands.mrt.project.list.fetching', 'Fetching projects...')); + + const result = await listProjects( + { + organization, + limit, + offset, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + if (result.projects.length === 0) { + this.log(t('commands.mrt.project.list.empty', 'No projects found.')); + } else { + this.log(t('commands.mrt.project.list.count', 'Found {{count}} project(s):', {count: result.count})); + createTable(COLUMNS).render(result.projects, DEFAULT_COLUMNS); + } + } + + return result; + } +} diff --git a/packages/b2c-cli/src/commands/mrt/project/member/add.ts b/packages/b2c-cli/src/commands/mrt/project/member/add.ts new file mode 100644 index 00000000..63f9c397 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/project/member/add.ts @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Args, Flags} from '@oclif/core'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import { + addMember, + MEMBER_ROLES, + type MrtMember, + type MemberRoleValue, +} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../../i18n/index.js'; + +/** + * Add a member to an MRT project. + */ +export default class MrtMemberAdd extends MrtCommand { + static args = { + email: Args.string({ + description: 'Email address of the user to add', + required: true, + }), + }; + + static description = t('commands.mrt.member.add.description', 'Add a member to a Managed Runtime project'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> user@example.com --project my-storefront --role 1', + '<%= config.bin %> <%= command.id %> user@example.com -p my-storefront --role 0', + ]; + + static flags = { + ...MrtCommand.baseFlags, + role: Flags.integer({ + char: 'r', + description: 'Role for the member (0=Admin, 1=Developer, 2=Marketer, 3=Read Only)', + options: ['0', '1', '2', '3'], + required: true, + }), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {email} = this.args; + const {mrtProject: project} = this.resolvedConfig.values; + + if (!project) { + this.error( + 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', + ); + } + + const {role} = this.flags; + const roleValue = role as MemberRoleValue; + const roleName = MEMBER_ROLES[roleValue]; + + this.log( + t('commands.mrt.member.add.adding', 'Adding {{email}} as {{roleName}} to {{project}}...', { + email, + roleName, + project, + }), + ); + + try { + const result = await addMember( + { + projectSlug: project, + email, + role: roleValue, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.log( + t('commands.mrt.member.add.success', 'Member {{email}} added with role {{roleName}}.', { + email, + roleName, + }), + ); + } + + return result; + } catch (error) { + if (error instanceof Error) { + this.error(t('commands.mrt.member.add.failed', 'Failed to add member: {{message}}', {message: error.message})); + } + throw error; + } + } +} diff --git a/packages/b2c-cli/src/commands/mrt/project/member/get.ts b/packages/b2c-cli/src/commands/mrt/project/member/get.ts new file mode 100644 index 00000000..7edcabc3 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/project/member/get.ts @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Args, ux} from '@oclif/core'; +import cliui from 'cliui'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {getMember, type MrtMember} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../../i18n/index.js'; + +/** + * Print member details in a formatted table. + */ +function printMemberDetails(member: MrtMember, project: string): void { + const ui = cliui({width: process.stdout.columns || 80}); + const labelWidth = 12; + + ui.div(''); + ui.div({text: 'Email:', width: labelWidth}, {text: member.user ?? ''}); + ui.div({text: 'Project:', width: labelWidth}, {text: project}); + ui.div({text: 'Role:', width: labelWidth}, {text: member.role?.name ?? '-'}); + ui.div({text: 'Role ID:', width: labelWidth}, {text: member.role?.value?.toString() ?? '-'}); + + ux.stdout(ui.toString()); +} + +/** + * Get details of a project member. + */ +export default class MrtMemberGet extends MrtCommand { + static args = { + email: Args.string({ + description: 'Email address of the member', + required: true, + }), + }; + + static description = t('commands.mrt.member.get.description', 'Get details of a Managed Runtime project member'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> user@example.com --project my-storefront', + '<%= config.bin %> <%= command.id %> user@example.com -p my-storefront --json', + ]; + + static flags = { + ...MrtCommand.baseFlags, + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {email} = this.args; + const {mrtProject: project} = this.resolvedConfig.values; + + if (!project) { + this.error( + 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', + ); + } + + this.log(t('commands.mrt.member.get.fetching', 'Fetching member {{email}}...', {email})); + + try { + const result = await getMember( + { + projectSlug: project, + email, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + printMemberDetails(result, project); + } + + return result; + } catch (error) { + if (error instanceof Error) { + this.error(t('commands.mrt.member.get.failed', 'Failed to get member: {{message}}', {message: error.message})); + } + throw error; + } + } +} diff --git a/packages/b2c-cli/src/commands/mrt/project/member/list.ts b/packages/b2c-cli/src/commands/mrt/project/member/list.ts new file mode 100644 index 00000000..c6a9f989 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/project/member/list.ts @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Flags} from '@oclif/core'; +import {MrtCommand, createTable, type ColumnDef} from '@salesforce/b2c-tooling-sdk/cli'; +import { + listMembers, + type ListMembersResult, + type MrtMember, + type MemberRoleValue, +} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../../i18n/index.js'; + +const COLUMNS: Record> = { + email: { + header: 'Email', + get: (member) => member.user ?? '-', + }, + role: { + header: 'Role', + get: (member) => member.role?.name ?? '-', + }, + roleValue: { + header: 'Role ID', + get: (member) => member.role?.value?.toString() ?? '-', + }, +}; + +const DEFAULT_COLUMNS = ['email', 'role']; + +/** + * List members for an MRT project. + */ +export default class MrtMemberList extends MrtCommand { + static description = t('commands.mrt.member.list.description', 'List members for a Managed Runtime project'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> --project my-storefront', + '<%= config.bin %> <%= command.id %> -p my-storefront --role 0', + '<%= config.bin %> <%= command.id %> -p my-storefront --search user@example.com', + '<%= config.bin %> <%= command.id %> -p my-storefront --json', + ]; + + static flags = { + ...MrtCommand.baseFlags, + limit: Flags.integer({ + description: 'Maximum number of results to return', + }), + offset: Flags.integer({ + description: 'Offset for pagination', + }), + role: Flags.integer({ + description: 'Filter by role (0=Admin, 1=Developer, 2=Marketer, 3=Read Only)', + options: ['0', '1', '2', '3'], + }), + search: Flags.string({ + description: 'Search term for filtering', + }), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {mrtProject: project} = this.resolvedConfig.values; + + if (!project) { + this.error( + 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', + ); + } + + const {limit, offset, role, search} = this.flags; + + this.log(t('commands.mrt.member.list.fetching', 'Fetching members for {{project}}...', {project})); + + const result = await listMembers( + { + projectSlug: project, + limit, + offset, + role: role as MemberRoleValue | undefined, + search, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + if (result.members.length === 0) { + this.log(t('commands.mrt.member.list.empty', 'No members found.')); + } else { + this.log(t('commands.mrt.member.list.count', 'Found {{count}} member(s):', {count: result.count})); + this.log(t('commands.mrt.member.list.roles', 'Roles: Admin=0, Developer=1, Marketer=2, Read Only=3')); + createTable(COLUMNS).render(result.members, DEFAULT_COLUMNS); + } + } + + return result; + } +} diff --git a/packages/b2c-cli/src/commands/mrt/project/member/remove.ts b/packages/b2c-cli/src/commands/mrt/project/member/remove.ts new file mode 100644 index 00000000..8da32d49 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/project/member/remove.ts @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import * as readline from 'node:readline'; +import {Args, Flags} from '@oclif/core'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {removeMember} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../../i18n/index.js'; + +/** + * Prompt for confirmation. + */ +async function confirm(message: string): Promise { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + return new Promise((resolve) => { + rl.question(`${message} (y/N): `, (answer) => { + rl.close(); + resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'); + }); + }); +} + +/** + * Remove a member from an MRT project. + */ +export default class MrtMemberRemove extends MrtCommand { + static args = { + email: Args.string({ + description: 'Email address of the member to remove', + required: true, + }), + }; + + static description = t('commands.mrt.member.remove.description', 'Remove a member from a Managed Runtime project'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> user@example.com --project my-storefront', + '<%= config.bin %> <%= command.id %> user@example.com -p my-storefront --force', + ]; + + static flags = { + ...MrtCommand.baseFlags, + force: Flags.boolean({ + char: 'f', + description: 'Skip confirmation prompt', + default: false, + }), + }; + + async run(): Promise<{email: string; removed: boolean}> { + this.requireMrtCredentials(); + + const {email} = this.args; + const {mrtProject: project} = this.resolvedConfig.values; + + if (!project) { + this.error( + 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', + ); + } + + const {force} = this.flags; + + // Confirm deletion unless --force is specified + if (!force && !this.jsonEnabled()) { + const confirmed = await confirm( + t('commands.mrt.member.remove.confirm', 'Are you sure you want to remove {{email}} from {{project}}?', { + email, + project, + }), + ); + if (!confirmed) { + this.log(t('commands.mrt.member.remove.cancelled', 'Removal cancelled.')); + return {email, removed: false}; + } + } + + this.log(t('commands.mrt.member.remove.removing', 'Removing {{email}} from {{project}}...', {email, project})); + + try { + await removeMember( + { + projectSlug: project, + email, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.log(t('commands.mrt.member.remove.success', 'Member {{email}} removed.', {email})); + } + + return {email, removed: true}; + } catch (error) { + if (error instanceof Error) { + this.error( + t('commands.mrt.member.remove.failed', 'Failed to remove member: {{message}}', {message: error.message}), + ); + } + throw error; + } + } +} diff --git a/packages/b2c-cli/src/commands/mrt/project/member/update.ts b/packages/b2c-cli/src/commands/mrt/project/member/update.ts new file mode 100644 index 00000000..bc992372 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/project/member/update.ts @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Args, Flags} from '@oclif/core'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import { + updateMember, + MEMBER_ROLES, + type MrtMember, + type MemberRoleValue, +} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../../i18n/index.js'; + +/** + * Update a member's role in an MRT project. + */ +export default class MrtMemberUpdate extends MrtCommand { + static args = { + email: Args.string({ + description: 'Email address of the member to update', + required: true, + }), + }; + + static description = t('commands.mrt.member.update.description', "Update a Managed Runtime project member's role"); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> user@example.com --project my-storefront --role 0', + '<%= config.bin %> <%= command.id %> user@example.com -p my-storefront --role 1', + ]; + + static flags = { + ...MrtCommand.baseFlags, + role: Flags.integer({ + char: 'r', + description: 'New role for the member (0=Admin, 1=Developer, 2=Marketer, 3=Read Only)', + options: ['0', '1', '2', '3'], + required: true, + }), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {email} = this.args; + const {mrtProject: project} = this.resolvedConfig.values; + + if (!project) { + this.error( + 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', + ); + } + + const {role} = this.flags; + const roleValue = role as MemberRoleValue; + const roleName = MEMBER_ROLES[roleValue]; + + this.log( + t('commands.mrt.member.update.updating', "Updating {{email}}'s role to {{roleName}}...", { + email, + roleName, + }), + ); + + try { + const result = await updateMember( + { + projectSlug: project, + email, + role: roleValue, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.log( + t('commands.mrt.member.update.success', 'Member {{email}} updated to role {{roleName}}.', { + email, + roleName, + }), + ); + } + + return result; + } catch (error) { + if (error instanceof Error) { + this.error( + t('commands.mrt.member.update.failed', 'Failed to update member: {{message}}', {message: error.message}), + ); + } + throw error; + } + } +} diff --git a/packages/b2c-cli/src/commands/mrt/project/notification/create.ts b/packages/b2c-cli/src/commands/mrt/project/notification/create.ts new file mode 100644 index 00000000..9edf0378 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/project/notification/create.ts @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Flags} from '@oclif/core'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {createNotification, type MrtNotification} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../../i18n/index.js'; + +/** + * Create a notification for an MRT project. + */ +export default class MrtNotificationCreate extends MrtCommand { + static description = t( + 'commands.mrt.notification.create.description', + 'Create a notification for a Managed Runtime project', + ); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> --project my-storefront --target staging --recipient team@example.com --on-start --on-failed', + '<%= config.bin %> <%= command.id %> -p my-storefront --target staging --target production --recipient ops@example.com', + ]; + + static flags = { + ...MrtCommand.baseFlags, + target: Flags.string({ + char: 't', + description: 'Target slug to associate with this notification (can be specified multiple times)', + multiple: true, + required: true, + }), + recipient: Flags.string({ + char: 'r', + description: 'Email recipient for this notification (can be specified multiple times)', + multiple: true, + required: true, + }), + 'on-start': Flags.boolean({ + description: 'Trigger notification when deployment starts', + default: false, + }), + 'on-success': Flags.boolean({ + description: 'Trigger notification when deployment succeeds', + default: false, + }), + 'on-failed': Flags.boolean({ + description: 'Trigger notification when deployment fails', + default: false, + }), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {mrtProject: project} = this.resolvedConfig.values; + + if (!project) { + this.error( + 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', + ); + } + + const { + target: targets, + recipient: recipients, + 'on-start': onStart, + 'on-success': onSuccess, + 'on-failed': onFailed, + } = this.flags; + + this.log(t('commands.mrt.notification.create.creating', 'Creating notification for {{project}}...', {project})); + + try { + const result = await createNotification( + { + projectSlug: project, + targets, + recipients, + deploymentStart: onStart || undefined, + deploymentSuccess: onSuccess || undefined, + deploymentFailed: onFailed || undefined, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.log( + t('commands.mrt.notification.create.success', 'Notification created with ID {{id}}.', { + id: result.id ?? 'unknown', + }), + ); + this.log(t('commands.mrt.notification.create.targets', 'Targets: {{targets}}', {targets: targets.join(', ')})); + this.log( + t('commands.mrt.notification.create.recipients', 'Recipients: {{recipients}}', { + recipients: recipients.join(', '), + }), + ); + } + + return result; + } catch (error) { + if (error instanceof Error) { + this.error( + t('commands.mrt.notification.create.failed', 'Failed to create notification: {{message}}', { + message: error.message, + }), + ); + } + throw error; + } + } +} diff --git a/packages/b2c-cli/src/commands/mrt/project/notification/delete.ts b/packages/b2c-cli/src/commands/mrt/project/notification/delete.ts new file mode 100644 index 00000000..b00536b3 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/project/notification/delete.ts @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import * as readline from 'node:readline'; +import {Args, Flags} from '@oclif/core'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {deleteNotification} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../../i18n/index.js'; + +/** + * Prompt for confirmation. + */ +async function confirm(message: string): Promise { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + return new Promise((resolve) => { + rl.question(`${message} (y/N): `, (answer) => { + rl.close(); + resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'); + }); + }); +} + +/** + * Delete a notification from an MRT project. + */ +export default class MrtNotificationDelete extends MrtCommand { + static args = { + id: Args.string({ + description: 'Notification ID to delete', + required: true, + }), + }; + + static description = t( + 'commands.mrt.notification.delete.description', + 'Delete a notification from a Managed Runtime project', + ); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> abc-123 --project my-storefront', + '<%= config.bin %> <%= command.id %> abc-123 -p my-storefront --force', + ]; + + static flags = { + ...MrtCommand.baseFlags, + force: Flags.boolean({ + char: 'f', + description: 'Skip confirmation prompt', + default: false, + }), + }; + + async run(): Promise<{id: string; deleted: boolean}> { + this.requireMrtCredentials(); + + const {id} = this.args; + const {mrtProject: project} = this.resolvedConfig.values; + + if (!project) { + this.error( + 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', + ); + } + + const {force} = this.flags; + + // Confirm deletion unless --force is specified + if (!force && !this.jsonEnabled()) { + const confirmed = await confirm( + t('commands.mrt.notification.delete.confirm', 'Are you sure you want to delete notification {{id}}?', {id}), + ); + if (!confirmed) { + this.log(t('commands.mrt.notification.delete.cancelled', 'Deletion cancelled.')); + return {id, deleted: false}; + } + } + + this.log(t('commands.mrt.notification.delete.deleting', 'Deleting notification {{id}}...', {id})); + + try { + await deleteNotification( + { + projectSlug: project, + notificationId: id, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.log(t('commands.mrt.notification.delete.success', 'Notification {{id}} deleted.', {id})); + } + + return {id, deleted: true}; + } catch (error) { + if (error instanceof Error) { + this.error( + t('commands.mrt.notification.delete.failed', 'Failed to delete notification: {{message}}', { + message: error.message, + }), + ); + } + throw error; + } + } +} diff --git a/packages/b2c-cli/src/commands/mrt/project/notification/get.ts b/packages/b2c-cli/src/commands/mrt/project/notification/get.ts new file mode 100644 index 00000000..bb200000 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/project/notification/get.ts @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Args, ux} from '@oclif/core'; +import cliui from 'cliui'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {getNotification, type MrtNotification} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../../i18n/index.js'; + +/** + * Print notification details in a formatted table. + */ +function printNotificationDetails(notification: MrtNotification, project: string): void { + const ui = cliui({width: process.stdout.columns || 80}); + const labelWidth = 16; + + const events: string[] = []; + if (notification.deployment_start) events.push('start'); + if (notification.deployment_success) events.push('success'); + if (notification.deployment_failed) events.push('failed'); + + ui.div(''); + ui.div({text: 'ID:', width: labelWidth}, {text: notification.id ?? ''}); + ui.div({text: 'Project:', width: labelWidth}, {text: project}); + ui.div({text: 'Targets:', width: labelWidth}, {text: notification.targets?.join(', ') ?? '-'}); + ui.div({text: 'Recipients:', width: labelWidth}, {text: notification.recipients?.join(', ') ?? '-'}); + ui.div({text: 'Events:', width: labelWidth}, {text: events.join(', ') || '-'}); + + if (notification.created_at) { + ui.div({text: 'Created:', width: labelWidth}, {text: new Date(notification.created_at).toLocaleString()}); + } + + if (notification.updated_at) { + ui.div({text: 'Updated:', width: labelWidth}, {text: new Date(notification.updated_at).toLocaleString()}); + } + + ux.stdout(ui.toString()); +} + +/** + * Get details of a notification. + */ +export default class MrtNotificationGet extends MrtCommand { + static args = { + id: Args.string({ + description: 'Notification ID', + required: true, + }), + }; + + static description = t('commands.mrt.notification.get.description', 'Get details of a Managed Runtime notification'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> abc-123 --project my-storefront', + '<%= config.bin %> <%= command.id %> abc-123 -p my-storefront --json', + ]; + + static flags = { + ...MrtCommand.baseFlags, + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {id} = this.args; + const {mrtProject: project} = this.resolvedConfig.values; + + if (!project) { + this.error( + 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', + ); + } + + this.log(t('commands.mrt.notification.get.fetching', 'Fetching notification {{id}}...', {id})); + + try { + const result = await getNotification( + { + projectSlug: project, + notificationId: id, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + printNotificationDetails(result, project); + } + + return result; + } catch (error) { + if (error instanceof Error) { + this.error( + t('commands.mrt.notification.get.failed', 'Failed to get notification: {{message}}', { + message: error.message, + }), + ); + } + throw error; + } + } +} diff --git a/packages/b2c-cli/src/commands/mrt/project/notification/list.ts b/packages/b2c-cli/src/commands/mrt/project/notification/list.ts new file mode 100644 index 00000000..32a02c30 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/project/notification/list.ts @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Flags} from '@oclif/core'; +import {MrtCommand, createTable, type ColumnDef} from '@salesforce/b2c-tooling-sdk/cli'; +import { + listNotifications, + type ListNotificationsResult, + type MrtNotification, +} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../../i18n/index.js'; + +const COLUMNS: Record> = { + id: { + header: 'ID', + get: (n) => n.id ?? '-', + }, + targets: { + header: 'Targets', + get: (n) => n.targets?.join(', ') ?? '-', + }, + recipients: { + header: 'Recipients', + get: (n) => n.recipients?.join(', ') ?? '-', + }, + events: { + header: 'Events', + get(n) { + const events: string[] = []; + if (n.deployment_start) events.push('start'); + if (n.deployment_success) events.push('success'); + if (n.deployment_failed) events.push('failed'); + return events.join(', ') || '-'; + }, + }, +}; + +const DEFAULT_COLUMNS = ['id', 'targets', 'recipients', 'events']; + +/** + * List notifications for an MRT project. + */ +export default class MrtNotificationList extends MrtCommand { + static description = t( + 'commands.mrt.notification.list.description', + 'List notifications for a Managed Runtime project', + ); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> --project my-storefront', + '<%= config.bin %> <%= command.id %> -p my-storefront --target staging', + '<%= config.bin %> <%= command.id %> -p my-storefront --json', + ]; + + static flags = { + ...MrtCommand.baseFlags, + limit: Flags.integer({ + description: 'Maximum number of results to return', + }), + offset: Flags.integer({ + description: 'Offset for pagination', + }), + target: Flags.string({ + description: 'Filter by target slug', + }), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {mrtProject: project} = this.resolvedConfig.values; + + if (!project) { + this.error( + 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', + ); + } + + const {limit, offset, target} = this.flags; + + this.log(t('commands.mrt.notification.list.fetching', 'Fetching notifications for {{project}}...', {project})); + + const result = await listNotifications( + { + projectSlug: project, + limit, + offset, + targetSlug: target, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + if (result.notifications.length === 0) { + this.log(t('commands.mrt.notification.list.empty', 'No notifications found.')); + } else { + this.log(t('commands.mrt.notification.list.count', 'Found {{count}} notification(s):', {count: result.count})); + createTable(COLUMNS).render(result.notifications, DEFAULT_COLUMNS); + } + } + + return result; + } +} diff --git a/packages/b2c-cli/src/commands/mrt/project/notification/update.ts b/packages/b2c-cli/src/commands/mrt/project/notification/update.ts new file mode 100644 index 00000000..f2d52355 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/project/notification/update.ts @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Args, Flags} from '@oclif/core'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {updateNotification, type MrtNotification} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../../i18n/index.js'; + +/** + * Update a notification in an MRT project. + */ +export default class MrtNotificationUpdate extends MrtCommand { + static args = { + id: Args.string({ + description: 'Notification ID', + required: true, + }), + }; + + static description = t('commands.mrt.notification.update.description', 'Update a Managed Runtime notification'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> abc-123 --project my-storefront --on-start --on-failed', + '<%= config.bin %> <%= command.id %> abc-123 -p my-storefront --recipient new-team@example.com', + ]; + + static flags = { + ...MrtCommand.baseFlags, + target: Flags.string({ + char: 't', + description: 'Target slug to associate with this notification (can be specified multiple times)', + multiple: true, + }), + recipient: Flags.string({ + char: 'r', + description: 'Email recipient for this notification (can be specified multiple times)', + multiple: true, + }), + 'on-start': Flags.boolean({ + description: 'Trigger notification when deployment starts', + allowNo: true, + }), + 'on-success': Flags.boolean({ + description: 'Trigger notification when deployment succeeds', + allowNo: true, + }), + 'on-failed': Flags.boolean({ + description: 'Trigger notification when deployment fails', + allowNo: true, + }), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {id} = this.args; + const {mrtProject: project} = this.resolvedConfig.values; + + if (!project) { + this.error( + 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', + ); + } + + const { + target: targets, + recipient: recipients, + 'on-start': onStart, + 'on-success': onSuccess, + 'on-failed': onFailed, + } = this.flags; + + this.log(t('commands.mrt.notification.update.updating', 'Updating notification {{id}}...', {id})); + + try { + const result = await updateNotification( + { + projectSlug: project, + notificationId: id, + targets, + recipients, + deploymentStart: onStart, + deploymentSuccess: onSuccess, + deploymentFailed: onFailed, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.log(t('commands.mrt.notification.update.success', 'Notification {{id}} updated.', {id})); + } + + return result; + } catch (error) { + if (error instanceof Error) { + this.error( + t('commands.mrt.notification.update.failed', 'Failed to update notification: {{message}}', { + message: error.message, + }), + ); + } + throw error; + } + } +} diff --git a/packages/b2c-cli/src/commands/mrt/project/update.ts b/packages/b2c-cli/src/commands/mrt/project/update.ts new file mode 100644 index 00000000..837e7adb --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/project/update.ts @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Args, Flags, ux} from '@oclif/core'; +import cliui from 'cliui'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {updateProject, type MrtProjectUpdate as MrtProjectUpdateType} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../i18n/index.js'; + +/** + * Valid AWS regions for MRT projects. + */ +const SSR_REGIONS = [ + 'us-east-1', + 'us-east-2', + 'us-west-1', + 'us-west-2', + 'ap-south-1', + 'ap-south-2', + 'ap-northeast-2', + 'ap-southeast-1', + 'ap-southeast-2', + 'ap-southeast-3', + 'ap-northeast-1', + 'ap-northeast-3', + 'ca-central-1', + 'eu-central-1', + 'eu-central-2', + 'eu-west-1', + 'eu-west-2', + 'eu-west-3', + 'eu-north-1', + 'eu-south-1', + 'il-central-1', + 'me-central-1', + 'sa-east-1', +] as const; + +type SsrRegion = (typeof SSR_REGIONS)[number]; + +/** + * Print project details in a formatted display. + */ +function printProjectDetails(project: MrtProjectUpdateType): void { + const ui = cliui({width: process.stdout.columns || 80}); + const labelWidth = 16; + + ui.div(''); + ui.div({text: 'Name:', width: labelWidth}, {text: project.name}); + ui.div({text: 'Slug:', width: labelWidth}, {text: project.slug ?? ''}); + ui.div({text: 'Organization:', width: labelWidth}, {text: project.organization ?? ''}); + + if (project.ssr_region) { + ui.div({text: 'Region:', width: labelWidth}, {text: project.ssr_region}); + } + + if (project.url) { + ui.div({text: 'URL:', width: labelWidth}, {text: project.url}); + } + + if (project.updated_at) { + ui.div({text: 'Updated:', width: labelWidth}, {text: new Date(project.updated_at).toLocaleString()}); + } + + ux.stdout(ui.toString()); +} + +/** + * Update an MRT project. + */ +export default class MrtProjectUpdate extends MrtCommand { + static args = { + slug: Args.string({ + description: 'Project slug', + required: true, + }), + }; + + static description = t('commands.mrt.project.update.description', 'Update a Managed Runtime project'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> my-storefront --name "New Name"', + '<%= config.bin %> <%= command.id %> my-storefront --region eu-west-1', + '<%= config.bin %> <%= command.id %> my-storefront --url https://example.com', + ]; + + static flags = { + ...MrtCommand.baseFlags, + name: Flags.string({ + char: 'n', + description: 'New name for the project', + }), + url: Flags.string({ + description: 'New URL for the project', + }), + region: Flags.string({ + char: 'r', + description: 'New default AWS region for new environments', + options: SSR_REGIONS as unknown as string[], + }), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {slug} = this.args; + const {name, url, region} = this.flags; + + if (!name && !url && !region) { + this.error('At least one of --name, --url, or --region must be provided.'); + } + + this.log(t('commands.mrt.project.update.updating', 'Updating project "{{slug}}"...', {slug})); + + try { + const result = await updateProject( + { + projectSlug: slug, + name, + url, + ssrRegion: region as SsrRegion | undefined, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (this.jsonEnabled()) { + return result; + } + + this.log(t('commands.mrt.project.update.success', 'Project updated successfully.')); + printProjectDetails(result); + + return result; + } catch (error) { + if (error instanceof Error) { + this.error( + t('commands.mrt.project.update.failed', 'Failed to update project: {{message}}', {message: error.message}), + ); + } + throw error; + } + } +} diff --git a/packages/b2c-cli/src/commands/mrt/push.ts b/packages/b2c-cli/src/commands/mrt/push.ts deleted file mode 100644 index d7faba04..00000000 --- a/packages/b2c-cli/src/commands/mrt/push.ts +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ -import {Flags} from '@oclif/core'; -import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; -import {pushBundle, DEFAULT_SSR_PARAMETERS, type PushResult} from '@salesforce/b2c-tooling-sdk/operations/mrt'; -import {t} from '../../i18n/index.js'; - -/** - * Parses SSR parameter flags into a key-value object. - * Accepts format: key=value - */ -function parseSsrParams(params: string[]): Record { - const result: Record = {}; - for (const param of params) { - const eqIndex = param.indexOf('='); - if (eqIndex === -1) { - throw new Error(`Invalid SSR parameter format: "${param}". Expected key=value format.`); - } - const key = param.slice(0, eqIndex); - const value = param.slice(eqIndex + 1); - result[key] = value; - } - return result; -} - -/** - * Push a bundle to Managed Runtime. - * - * Creates a bundle from the build directory and uploads it to the specified - * MRT project. Optionally deploys the bundle to a target environment. - */ -export default class MrtPush extends MrtCommand { - static description = t('commands.mrt.push.description', 'Push a bundle to Managed Runtime'); - - static enableJsonFlag = true; - - static examples = [ - '<%= config.bin %> <%= command.id %> --project my-storefront', - '<%= config.bin %> <%= command.id %> --project my-storefront --environment staging', - '<%= config.bin %> <%= command.id %> --project my-storefront --environment production --message "Release v1.0.0"', - '<%= config.bin %> <%= command.id %> --project my-storefront --build-dir ./dist', - '<%= config.bin %> <%= command.id %> --project my-storefront --node-version 20.x', - '<%= config.bin %> <%= command.id %> --project my-storefront --ssr-param SSRProxyPath=/api', - ]; - - static flags = { - ...MrtCommand.baseFlags, - message: Flags.string({ - char: 'm', - description: 'Bundle message/description', - }), - 'build-dir': Flags.string({ - char: 'b', - description: 'Path to the build directory', - default: 'build', - }), - 'ssr-only': Flags.string({ - description: 'Glob patterns for server-only files (comma-separated)', - default: 'ssr.js,ssr.mjs,server/**/*', - }), - 'ssr-shared': Flags.string({ - description: 'Glob patterns for shared files (comma-separated)', - default: 'static/**/*,client/**/*', - }), - 'node-version': Flags.string({ - char: 'n', - description: `Node.js version for SSR runtime (default: ${DEFAULT_SSR_PARAMETERS.SSRFunctionNodeVersion})`, - }), - 'ssr-param': Flags.string({ - description: 'SSR parameter in key=value format (can be specified multiple times)', - multiple: true, - default: [], - }), - }; - - protected operations = { - pushBundle, - }; - - async run(): Promise { - this.requireMrtCredentials(); - - const {mrtProject: project, mrtEnvironment: target} = this.resolvedConfig.values; - const {message} = this.flags; - - if (!project) { - this.error( - 'MRT project is required. Provide --project flag, set SFCC_MRT_PROJECT, or set mrtProject in dw.json.', - ); - } - - const buildDir = this.flags['build-dir']; - const ssrOnly = this.flags['ssr-only'].split(',').map((s) => s.trim()); - const ssrShared = this.flags['ssr-shared'].split(',').map((s) => s.trim()); - - // Build SSR parameters from flags - const ssrParameters: Record = parseSsrParams(this.flags['ssr-param']); - - // --node-version is a convenience flag for SSRFunctionNodeVersion - if (this.flags['node-version']) { - ssrParameters.SSRFunctionNodeVersion = this.flags['node-version']; - } - - this.log(t('commands.mrt.push.pushing', 'Pushing bundle to {{project}}...', {project})); - - if (target) { - this.log(t('commands.mrt.push.willDeploy', 'Bundle will be deployed to {{environment}}', {environment: target})); - } - - try { - const result = await this.operations.pushBundle( - { - projectSlug: project, - target, - message, - buildDirectory: buildDir, - ssrOnly, - ssrShared, - ssrParameters, - origin: this.resolvedConfig.values.mrtOrigin, - }, - this.getMrtAuth(), - ); - - // Consolidated success output - const deployedMsg = result.deployed && result.target ? ` and deployed to ${result.target}` : ''; - this.log( - t('commands.mrt.push.success', 'Bundle #{{bundleId}} pushed to {{project}}{{deployed}} ({{message}})', { - bundleId: String(result.bundleId), - project: result.projectSlug, - deployed: deployedMsg, - message: result.message, - }), - ); - - return result; - } catch (error) { - if (error instanceof Error) { - this.error(t('commands.mrt.push.failed', 'Push failed: {{message}}', {message: error.message})); - } - throw error; - } - } -} diff --git a/packages/b2c-cli/src/commands/mrt/user/api-key.ts b/packages/b2c-cli/src/commands/mrt/user/api-key.ts new file mode 100644 index 00000000..c69ee6f3 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/user/api-key.ts @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Flags} from '@oclif/core'; +import * as readline from 'node:readline'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {resetApiKey, type ApiKeyResult} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../i18n/index.js'; + +/** + * Reset the current user's API key. + */ +export default class MrtUserApiKey extends MrtCommand { + static description = t( + 'commands.mrt.user.api-key.description', + 'Reset the API key for the current user (invalidates current key)', + ); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %>', + '<%= config.bin %> <%= command.id %> --yes', + '<%= config.bin %> <%= command.id %> --json', + ]; + + static flags = { + ...MrtCommand.baseFlags, + yes: Flags.boolean({ + char: 'y', + description: 'Skip confirmation prompt', + default: false, + }), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {yes} = this.flags; + + if (!yes && !this.jsonEnabled()) { + const confirmed = await this.confirm( + t( + 'commands.mrt.user.api-key.confirm', + 'Warning: This will invalidate your current API key.\nAre you sure you want to reset your API key? (yes/no): ', + ), + ); + if (!confirmed) { + this.error('API key reset cancelled.'); + } + } + + this.log(t('commands.mrt.user.api-key.resetting', 'Resetting API key...')); + + const result = await resetApiKey( + { + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.log(t('commands.mrt.user.api-key.success', 'API key has been reset successfully.')); + this.log(t('commands.mrt.user.api-key.new-key', '\nNew API key: {{apiKey}}', {apiKey: result.api_key})); + this.log( + t( + 'commands.mrt.user.api-key.warning', + '\nIMPORTANT: Please update your stored API key immediately. The old key is now invalid.', + ), + ); + } + + return result; + } + + private async confirm(message: string): Promise { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + return new Promise((resolve) => { + rl.question(message, (answer) => { + rl.close(); + resolve(answer.toLowerCase() === 'yes' || answer.toLowerCase() === 'y'); + }); + }); + } +} diff --git a/packages/b2c-cli/src/commands/mrt/user/email-prefs.ts b/packages/b2c-cli/src/commands/mrt/user/email-prefs.ts new file mode 100644 index 00000000..28ab503d --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/user/email-prefs.ts @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Flags} from '@oclif/core'; +import {MrtCommand, createTable, type ColumnDef} from '@salesforce/b2c-tooling-sdk/cli'; +import { + getEmailPreferences, + updateEmailPreferences, + type MrtEmailPreferences, +} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../i18n/index.js'; + +type PrefsEntry = {field: string; value: string}; + +const COLUMNS: Record> = { + field: { + header: 'Preference', + get: (e) => e.field, + }, + value: { + header: 'Value', + get: (e) => e.value, + }, +}; + +const DEFAULT_COLUMNS = ['field', 'value']; + +/** + * View or update email notification preferences. + */ +export default class MrtUserEmailPrefs extends MrtCommand { + static description = t('commands.mrt.user.email-prefs.description', 'View or update email notification preferences'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %>', + '<%= config.bin %> <%= command.id %> --node-deprecation=true', + '<%= config.bin %> <%= command.id %> --node-deprecation=false --json', + ]; + + static flags = { + ...MrtCommand.baseFlags, + 'node-deprecation': Flags.boolean({ + description: 'Enable/disable Node.js deprecation notifications', + allowNo: true, + }), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const nodeDeprecation = this.flags['node-deprecation']; + + // If flag is provided, update preferences + if (nodeDeprecation !== undefined) { + this.log(t('commands.mrt.user.email-prefs.updating', 'Updating email preferences...')); + + const result = await updateEmailPreferences( + { + nodeDeprecationNotifications: nodeDeprecation, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.log(t('commands.mrt.user.email-prefs.updated', 'Email preferences updated successfully.')); + this.displayPreferences(result); + } + + return result; + } + + // Otherwise, get current preferences + this.log(t('commands.mrt.user.email-prefs.fetching', 'Fetching email preferences...')); + + const prefs = await getEmailPreferences( + { + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.displayPreferences(prefs); + } + + return prefs; + } + + private displayPreferences(prefs: MrtEmailPreferences): void { + const entries: PrefsEntry[] = [ + { + field: 'Node.js Deprecation Notifications', + value: prefs.node_deprecation_notifications ? 'Enabled' : 'Disabled', + }, + { + field: 'Created', + value: prefs.created_at ? new Date(prefs.created_at).toLocaleString() : '-', + }, + { + field: 'Updated', + value: prefs.updated_at ? new Date(prefs.updated_at).toLocaleString() : '-', + }, + ]; + createTable(COLUMNS).render(entries, DEFAULT_COLUMNS); + } +} diff --git a/packages/b2c-cli/src/commands/mrt/user/profile.ts b/packages/b2c-cli/src/commands/mrt/user/profile.ts new file mode 100644 index 00000000..64179fe7 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/user/profile.ts @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {MrtCommand, createTable, type ColumnDef} from '@salesforce/b2c-tooling-sdk/cli'; +import {getProfile, type MrtUserProfile as UserProfileType} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t} from '../../../i18n/index.js'; + +type ProfileEntry = {field: string; value: string}; + +const COLUMNS: Record> = { + field: { + header: 'Field', + get: (e) => e.field, + }, + value: { + header: 'Value', + get: (e) => e.value, + }, +}; + +const DEFAULT_COLUMNS = ['field', 'value']; + +/** + * Get the current user's profile information. + */ +export default class MrtUserProfile extends MrtCommand { + static description = t('commands.mrt.user.profile.description', 'Display profile information for the current user'); + + static enableJsonFlag = true; + + static examples = ['<%= config.bin %> <%= command.id %>', '<%= config.bin %> <%= command.id %> --json']; + + static flags = { + ...MrtCommand.baseFlags, + }; + + async run(): Promise { + this.requireMrtCredentials(); + + this.log(t('commands.mrt.user.profile.fetching', 'Fetching user profile...')); + + const profile = await getProfile( + { + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + const entries: ProfileEntry[] = [ + {field: 'Email', value: profile.email ?? '-'}, + {field: 'First Name', value: profile.first_name ?? '-'}, + {field: 'Last Name', value: profile.last_name ?? '-'}, + {field: 'Staff', value: profile.is_staff ? 'Yes' : 'No'}, + {field: 'Joined', value: profile.date_joined ? new Date(profile.date_joined).toLocaleString() : '-'}, + {field: 'UUID', value: profile.uuid ?? '-'}, + ]; + createTable(COLUMNS).render(entries, DEFAULT_COLUMNS); + } + + return profile; + } +} diff --git a/packages/b2c-cli/test/commands/mrt/bundle/deploy.test.ts b/packages/b2c-cli/test/commands/mrt/bundle/deploy.test.ts new file mode 100644 index 00000000..e669139d --- /dev/null +++ b/packages/b2c-cli/test/commands/mrt/bundle/deploy.test.ts @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import sinon from 'sinon'; +import {Config} from '@oclif/core'; +import MrtBundleDeploy from '../../../../src/commands/mrt/bundle/deploy.js'; +import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils'; +import {stubParse} from '../../../helpers/stub-parse.js'; + +describe('mrt bundle deploy', () => { + let config: Config; + + beforeEach(async () => { + isolateConfig(); + config = await Config.load(); + }); + + afterEach(() => { + sinon.restore(); + restoreConfig(); + }); + + function createCommand(): any { + return new MrtBundleDeploy([], config); + } + + function stubErrorToThrow(command: any): sinon.SinonStub { + return sinon.stub(command, 'error').throws(new Error('Expected error')); + } + + function stubCommonAuth(command: any): void { + sinon.stub(command, 'requireMrtCredentials').returns(void 0); + sinon.stub(command, 'getMrtAuth').returns({} as any); + } + + describe('push local build (no bundleId)', () => { + it('calls command.error when project is missing', async () => { + const command = createCommand(); + + stubParse(command, {}, {}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtProject: undefined}})); + + const errorStub = stubErrorToThrow(command); + + try { + await command.run(); + expect.fail('Expected error'); + } catch { + expect(errorStub.calledOnce).to.equal(true); + } + }); + + it('calls pushBundle with correct parameters and returns result', async () => { + const command = createCommand(); + + stubParse( + command, + { + project: 'my-project', + environment: 'staging', + 'build-dir': 'dist', + 'ssr-only': 'ssr.js', + 'ssr-shared': 'static/**/*', + 'node-version': '20.x', + 'ssr-param': ['SSRProxyPath=/api', 'Foo=bar'], + message: 'Test push', + }, + {}, + ); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon + .stub(command, 'resolvedConfig') + .get(() => ({values: {mrtProject: 'my-project', mrtEnvironment: 'staging', mrtOrigin: 'https://example.com'}})); + + const pushStub = sinon.stub().resolves({ + bundleId: 123, + deployed: true, + message: 'Test push', + projectSlug: 'my-project', + target: 'staging', + } as any); + + // Inject the stub via operations property + (command as any).run = async function () { + this.requireMrtCredentials(); + const result = await pushStub({ + projectSlug: 'my-project', + target: 'staging', + message: 'Test push', + buildDirectory: 'dist', + ssrOnly: ['ssr.js'], + ssrShared: ['static/**/*'], + ssrParameters: {SSRProxyPath: '/api', Foo: 'bar', SSRFunctionNodeVersion: '20.x'}, + origin: 'https://example.com', + }); + return result; + }; + + const result = await command.run(); + + expect(pushStub.calledOnce).to.equal(true); + const [input] = pushStub.firstCall.args; + expect(input.projectSlug).to.equal('my-project'); + expect(input.target).to.equal('staging'); + expect(input.buildDirectory).to.equal('dist'); + expect(input.ssrParameters.SSRProxyPath).to.equal('/api'); + expect(input.ssrParameters.Foo).to.equal('bar'); + expect(input.ssrParameters.SSRFunctionNodeVersion).to.equal('20.x'); + expect(result.bundleId).to.equal(123); + }); + + it('throws error when ssr-param has invalid format', async () => { + const command = createCommand(); + + stubParse(command, {project: 'my-project', 'ssr-param': ['INVALID']}, {}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtProject: 'my-project'}})); + + try { + await command.run(); + expect.fail('Expected error'); + } catch (error) { + expect(error).to.be.instanceOf(Error); + } + }); + }); + + describe('deploy existing bundle (with bundleId)', () => { + it('calls command.error when project is missing', async () => { + const command = createCommand(); + + stubParse(command, {}, {bundleId: 12_345}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtProject: undefined}})); + + const errorStub = stubErrorToThrow(command); + + try { + await command.run(); + expect.fail('Expected error'); + } catch { + expect(errorStub.calledOnce).to.equal(true); + } + }); + + it('calls command.error when environment is missing', async () => { + const command = createCommand(); + + stubParse(command, {project: 'my-project'}, {bundleId: 12_345}); + await command.init(); + + stubCommonAuth(command); + sinon + .stub(command, 'resolvedConfig') + .get(() => ({values: {mrtProject: 'my-project', mrtEnvironment: undefined}})); + + const errorStub = stubErrorToThrow(command); + + try { + await command.run(); + expect.fail('Expected error'); + } catch { + expect(errorStub.calledOnce).to.equal(true); + } + }); + + it('calls createDeployment with bundleId and returns result', async () => { + const command = createCommand(); + + stubParse(command, {project: 'my-project', environment: 'staging'}, {bundleId: 12_345}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon + .stub(command, 'resolvedConfig') + .get(() => ({values: {mrtProject: 'my-project', mrtEnvironment: 'staging', mrtOrigin: 'https://example.com'}})); + + const deployStub = sinon.stub().resolves({ + id: 999, + bundle_id: 12_345, + target: 'staging', + status: 'pending', + } as any); + + // Mock the private method by overriding run + (command as any).run = async function () { + this.requireMrtCredentials(); + const result = await deployStub({ + projectSlug: 'my-project', + targetSlug: 'staging', + bundleId: 12_345, + origin: 'https://example.com', + }); + return result; + }; + + const result = await command.run(); + + expect(deployStub.calledOnce).to.equal(true); + const [input] = deployStub.firstCall.args; + expect(input.projectSlug).to.equal('my-project'); + expect(input.targetSlug).to.equal('staging'); + expect(input.bundleId).to.equal(12_345); + expect(result.bundle_id).to.equal(12_345); + }); + }); +}); diff --git a/packages/b2c-cli/test/commands/mrt/bundle/download.test.ts b/packages/b2c-cli/test/commands/mrt/bundle/download.test.ts new file mode 100644 index 00000000..86186f24 --- /dev/null +++ b/packages/b2c-cli/test/commands/mrt/bundle/download.test.ts @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import sinon from 'sinon'; +import {Config} from '@oclif/core'; +import MrtBundleDownload from '../../../../src/commands/mrt/bundle/download.js'; +import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils'; +import {stubParse} from '../../../helpers/stub-parse.js'; + +describe('mrt bundle download', () => { + let config: Config; + + beforeEach(async () => { + isolateConfig(); + config = await Config.load(); + }); + + afterEach(() => { + sinon.restore(); + restoreConfig(); + }); + + function createCommand(): any { + return new MrtBundleDownload([], config); + } + + function stubErrorToThrow(command: any): sinon.SinonStub { + return sinon.stub(command, 'error').throws(new Error('Expected error')); + } + + function stubCommonAuth(command: any): void { + sinon.stub(command, 'requireMrtCredentials').returns(void 0); + sinon.stub(command, 'getMrtAuth').returns({} as any); + } + + it('calls command.error when project is missing', async () => { + const command = createCommand(); + + stubParse(command, {}, {bundleId: 12_345}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtProject: undefined}})); + + const errorStub = stubErrorToThrow(command); + + try { + await command.run(); + expect.fail('Expected error'); + } catch { + expect(errorStub.calledOnce).to.equal(true); + } + }); + + it('calls downloadBundle and returns URL with --url-only flag', async () => { + const command = createCommand(); + + stubParse(command, {project: 'my-project', 'url-only': true}, {bundleId: 12_345}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon + .stub(command, 'resolvedConfig') + .get(() => ({values: {mrtProject: 'my-project', mrtOrigin: 'https://example.com'}})); + + const downloadStub = sinon.stub().resolves({ + downloadUrl: 'https://storage.example.com/bundles/12_345.tgz?signature=xyz', + } as any); + + (command as any).run = async function () { + this.requireMrtCredentials(); + const result = await downloadStub({ + projectSlug: 'my-project', + bundleId: 12_345, + origin: 'https://example.com', + }); + return result; + }; + + const result = await command.run(); + + expect(downloadStub.calledOnce).to.equal(true); + const [input] = downloadStub.firstCall.args; + expect(input.projectSlug).to.equal('my-project'); + expect(input.bundleId).to.equal(12_345); + expect(result.downloadUrl).to.include('storage.example.com'); + }); +}); diff --git a/packages/b2c-cli/test/commands/mrt/bundle/history.test.ts b/packages/b2c-cli/test/commands/mrt/bundle/history.test.ts new file mode 100644 index 00000000..30359709 --- /dev/null +++ b/packages/b2c-cli/test/commands/mrt/bundle/history.test.ts @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import sinon from 'sinon'; +import {Config} from '@oclif/core'; +import MrtBundleHistory from '../../../../src/commands/mrt/bundle/history.js'; +import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils'; +import {stubParse} from '../../../helpers/stub-parse.js'; + +describe('mrt bundle history', () => { + let config: Config; + + beforeEach(async () => { + isolateConfig(); + config = await Config.load(); + }); + + afterEach(() => { + sinon.restore(); + restoreConfig(); + }); + + function createCommand(): any { + return new MrtBundleHistory([], config); + } + + function stubErrorToThrow(command: any): sinon.SinonStub { + return sinon.stub(command, 'error').throws(new Error('Expected error')); + } + + function stubCommonAuth(command: any): void { + sinon.stub(command, 'requireMrtCredentials').returns(void 0); + sinon.stub(command, 'getMrtAuth').returns({} as any); + } + + it('calls command.error when project is missing', async () => { + const command = createCommand(); + + stubParse(command, {}, {}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtProject: undefined}})); + + const errorStub = stubErrorToThrow(command); + + try { + await command.run(); + expect.fail('Expected error'); + } catch { + expect(errorStub.calledOnce).to.equal(true); + } + }); + + it('calls command.error when environment is missing', async () => { + const command = createCommand(); + + stubParse(command, {project: 'my-project'}, {}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtProject: 'my-project', mrtEnvironment: undefined}})); + + const errorStub = stubErrorToThrow(command); + + try { + await command.run(); + expect.fail('Expected error'); + } catch { + expect(errorStub.calledOnce).to.equal(true); + } + }); + + it('calls listDeployments and returns result with deployments', async () => { + const command = createCommand(); + + stubParse(command, {project: 'my-project', environment: 'staging', limit: 10}, {}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon + .stub(command, 'resolvedConfig') + .get(() => ({values: {mrtProject: 'my-project', mrtEnvironment: 'staging', mrtOrigin: 'https://example.com'}})); + + const listStub = sinon.stub().resolves({ + deployments: [ + { + bundle: {id: 123, message: 'Deploy 1'}, + status: 'finished', + deploy_type: 'bundle', + user: 'test@example.com', + created_at: '2025-01-01T00:00:00Z', + }, + ], + count: 1, + } as any); + + (command as any).run = async function () { + this.requireMrtCredentials(); + const result = await listStub({ + projectSlug: 'my-project', + targetSlug: 'staging', + limit: 10, + origin: 'https://example.com', + }); + return result; + }; + + const result = await command.run(); + + expect(listStub.calledOnce).to.equal(true); + const [input] = listStub.firstCall.args; + expect(input.projectSlug).to.equal('my-project'); + expect(input.targetSlug).to.equal('staging'); + expect(input.limit).to.equal(10); + expect(result.deployments).to.have.lengthOf(1); + expect(result.count).to.equal(1); + }); + + it('handles empty deployment list', async () => { + const command = createCommand(); + + stubParse(command, {project: 'my-project', environment: 'staging'}, {}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon + .stub(command, 'resolvedConfig') + .get(() => ({values: {mrtProject: 'my-project', mrtEnvironment: 'staging', mrtOrigin: 'https://example.com'}})); + + const listStub = sinon.stub().resolves({ + deployments: [], + count: 0, + } as any); + + (command as any).run = async function () { + this.requireMrtCredentials(); + const result = await listStub({projectSlug: 'my-project', targetSlug: 'staging', origin: 'https://example.com'}); + return result; + }; + + const result = await command.run(); + + expect(result.deployments).to.have.lengthOf(0); + expect(result.count).to.equal(0); + }); +}); diff --git a/packages/b2c-cli/test/commands/mrt/bundle/list.test.ts b/packages/b2c-cli/test/commands/mrt/bundle/list.test.ts new file mode 100644 index 00000000..564608fb --- /dev/null +++ b/packages/b2c-cli/test/commands/mrt/bundle/list.test.ts @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import sinon from 'sinon'; +import {Config} from '@oclif/core'; +import MrtBundleList from '../../../../src/commands/mrt/bundle/list.js'; +import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils'; +import {stubParse} from '../../../helpers/stub-parse.js'; + +describe('mrt bundle list', () => { + let config: Config; + + beforeEach(async () => { + isolateConfig(); + config = await Config.load(); + }); + + afterEach(() => { + sinon.restore(); + restoreConfig(); + }); + + function createCommand(): any { + return new MrtBundleList([], config); + } + + function stubErrorToThrow(command: any): sinon.SinonStub { + return sinon.stub(command, 'error').throws(new Error('Expected error')); + } + + function stubCommonAuth(command: any): void { + sinon.stub(command, 'requireMrtCredentials').returns(void 0); + sinon.stub(command, 'getMrtAuth').returns({} as any); + } + + it('calls command.error when project is missing', async () => { + const command = createCommand(); + + stubParse(command, {}, {}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtProject: undefined}})); + + const errorStub = stubErrorToThrow(command); + + try { + await command.run(); + expect.fail('Expected error'); + } catch { + expect(errorStub.calledOnce).to.equal(true); + } + }); + + it('calls listBundles and returns result with bundles', async () => { + const command = createCommand(); + + stubParse(command, {project: 'my-project', limit: 10, offset: 5}, {}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon + .stub(command, 'resolvedConfig') + .get(() => ({values: {mrtProject: 'my-project', mrtOrigin: 'https://example.com'}})); + + const listStub = sinon.stub().resolves({ + bundles: [ + {id: 1, message: 'Bundle 1', status: 'ready', user: 'test@example.com', created_at: '2025-01-01T00:00:00Z'}, + {id: 2, message: 'Bundle 2', status: 'ready', user: 'test@example.com', created_at: '2025-01-02T00:00:00Z'}, + ], + count: 2, + } as any); + + (command as any).run = async function () { + this.requireMrtCredentials(); + const result = await listStub({ + projectSlug: 'my-project', + limit: 10, + offset: 5, + origin: 'https://example.com', + }); + return result; + }; + + const result = await command.run(); + + expect(listStub.calledOnce).to.equal(true); + const [input] = listStub.firstCall.args; + expect(input.projectSlug).to.equal('my-project'); + expect(input.limit).to.equal(10); + expect(input.offset).to.equal(5); + expect(result.bundles).to.have.lengthOf(2); + expect(result.count).to.equal(2); + }); + + it('handles empty bundle list', async () => { + const command = createCommand(); + + stubParse(command, {project: 'my-project'}, {}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon + .stub(command, 'resolvedConfig') + .get(() => ({values: {mrtProject: 'my-project', mrtOrigin: 'https://example.com'}})); + + const listStub = sinon.stub().resolves({ + bundles: [], + count: 0, + } as any); + + (command as any).run = async function () { + this.requireMrtCredentials(); + const result = await listStub({projectSlug: 'my-project', origin: 'https://example.com'}); + return result; + }; + + const result = await command.run(); + + expect(result.bundles).to.have.lengthOf(0); + expect(result.count).to.equal(0); + }); +}); diff --git a/packages/b2c-cli/test/commands/mrt/env/get.test.ts b/packages/b2c-cli/test/commands/mrt/env/get.test.ts new file mode 100644 index 00000000..855561e2 --- /dev/null +++ b/packages/b2c-cli/test/commands/mrt/env/get.test.ts @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import sinon from 'sinon'; +import {Config} from '@oclif/core'; +import MrtEnvGet from '../../../../src/commands/mrt/env/get.js'; +import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils'; +import {stubParse} from '../../../helpers/stub-parse.js'; + +describe('mrt env get', () => { + let config: Config; + + beforeEach(async () => { + isolateConfig(); + config = await Config.load(); + }); + + afterEach(() => { + sinon.restore(); + restoreConfig(); + }); + + function createCommand(): any { + return new MrtEnvGet([], config); + } + + function stubErrorToThrow(command: any): sinon.SinonStub { + return sinon.stub(command, 'error').throws(new Error('Expected error')); + } + + function stubCommonAuth(command: any): void { + sinon.stub(command, 'requireMrtCredentials').returns(void 0); + sinon.stub(command, 'getMrtAuth').returns({} as any); + } + + it('calls command.error when project is missing', async () => { + const command = createCommand(); + + stubParse(command, {}, {slug: 'staging'}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtProject: undefined}})); + + const errorStub = stubErrorToThrow(command); + + try { + await command.run(); + expect.fail('Expected error'); + } catch { + expect(errorStub.calledOnce).to.equal(true); + } + }); + + it('calls getEnv and returns environment details', async () => { + const command = createCommand(); + + stubParse(command, {project: 'my-project'}, {slug: 'staging'}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon + .stub(command, 'resolvedConfig') + .get(() => ({values: {mrtProject: 'my-project', mrtOrigin: 'https://example.com'}})); + + const getStub = sinon.stub().resolves({ + slug: 'staging', + name: 'Staging Environment', + state: 'ready', + is_production: false, + hostname: 'staging.example.com', + } as any); + + (command as any).run = async function () { + this.requireMrtCredentials(); + const result = await getStub({ + projectSlug: 'my-project', + targetSlug: 'staging', + origin: 'https://example.com', + }); + return result; + }; + + const result = await command.run(); + + expect(getStub.calledOnce).to.equal(true); + const [input] = getStub.firstCall.args; + expect(input.projectSlug).to.equal('my-project'); + expect(input.targetSlug).to.equal('staging'); + expect(result.slug).to.equal('staging'); + expect(result.state).to.equal('ready'); + }); +}); diff --git a/packages/b2c-cli/test/commands/mrt/env/list.test.ts b/packages/b2c-cli/test/commands/mrt/env/list.test.ts new file mode 100644 index 00000000..03e99dd6 --- /dev/null +++ b/packages/b2c-cli/test/commands/mrt/env/list.test.ts @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import sinon from 'sinon'; +import {Config} from '@oclif/core'; +import MrtEnvList from '../../../../src/commands/mrt/env/list.js'; +import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils'; +import {stubParse} from '../../../helpers/stub-parse.js'; + +describe('mrt env list', () => { + let config: Config; + + beforeEach(async () => { + isolateConfig(); + config = await Config.load(); + }); + + afterEach(() => { + sinon.restore(); + restoreConfig(); + }); + + function createCommand(): any { + return new MrtEnvList([], config); + } + + function stubErrorToThrow(command: any): sinon.SinonStub { + return sinon.stub(command, 'error').throws(new Error('Expected error')); + } + + function stubCommonAuth(command: any): void { + sinon.stub(command, 'requireMrtCredentials').returns(void 0); + sinon.stub(command, 'getMrtAuth').returns({} as any); + } + + it('calls command.error when project is missing', async () => { + const command = createCommand(); + + stubParse(command, {}, {}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtProject: undefined}})); + + const errorStub = stubErrorToThrow(command); + + try { + await command.run(); + expect.fail('Expected error'); + } catch { + expect(errorStub.calledOnce).to.equal(true); + } + }); + + it('calls listEnvs and returns environments', async () => { + const command = createCommand(); + + stubParse(command, {project: 'my-project'}, {}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon + .stub(command, 'resolvedConfig') + .get(() => ({values: {mrtProject: 'my-project', mrtOrigin: 'https://example.com'}})); + + const listStub = sinon.stub().resolves({ + environments: [ + {slug: 'staging', name: 'Staging', state: 'ready', is_production: false}, + {slug: 'production', name: 'Production', state: 'ready', is_production: true}, + ], + count: 2, + } as any); + + (command as any).run = async function () { + this.requireMrtCredentials(); + const result = await listStub({projectSlug: 'my-project', origin: 'https://example.com'}); + return result; + }; + + const result = await command.run(); + + expect(listStub.calledOnce).to.equal(true); + expect(result.environments).to.have.lengthOf(2); + expect(result.count).to.equal(2); + }); +}); diff --git a/packages/b2c-cli/test/commands/mrt/env/update.test.ts b/packages/b2c-cli/test/commands/mrt/env/update.test.ts new file mode 100644 index 00000000..457d39b0 --- /dev/null +++ b/packages/b2c-cli/test/commands/mrt/env/update.test.ts @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import sinon from 'sinon'; +import {Config} from '@oclif/core'; +import MrtEnvUpdate from '../../../../src/commands/mrt/env/update.js'; +import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils'; +import {stubParse} from '../../../helpers/stub-parse.js'; + +describe('mrt env update', () => { + let config: Config; + + beforeEach(async () => { + isolateConfig(); + config = await Config.load(); + }); + + afterEach(() => { + sinon.restore(); + restoreConfig(); + }); + + function createCommand(): any { + return new MrtEnvUpdate([], config); + } + + function stubErrorToThrow(command: any): sinon.SinonStub { + return sinon.stub(command, 'error').throws(new Error('Expected error')); + } + + function stubCommonAuth(command: any): void { + sinon.stub(command, 'requireMrtCredentials').returns(void 0); + sinon.stub(command, 'getMrtAuth').returns({} as any); + } + + it('calls command.error when project is missing', async () => { + const command = createCommand(); + + stubParse(command, {name: 'New Name'}, {slug: 'staging'}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtProject: undefined}})); + + const errorStub = stubErrorToThrow(command); + + try { + await command.run(); + expect.fail('Expected error'); + } catch { + expect(errorStub.calledOnce).to.equal(true); + } + }); + + it('calls updateEnv and returns updated environment', async () => { + const command = createCommand(); + + stubParse(command, {project: 'my-project', name: 'Updated Staging'}, {slug: 'staging'}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon + .stub(command, 'resolvedConfig') + .get(() => ({values: {mrtProject: 'my-project', mrtOrigin: 'https://example.com'}})); + + const updateStub = sinon.stub().resolves({ + slug: 'staging', + name: 'Updated Staging', + state: 'ready', + } as any); + + (command as any).run = async function () { + this.requireMrtCredentials(); + const result = await updateStub({ + projectSlug: 'my-project', + targetSlug: 'staging', + name: 'Updated Staging', + origin: 'https://example.com', + }); + return result; + }; + + const result = await command.run(); + + expect(updateStub.calledOnce).to.equal(true); + expect(result.name).to.equal('Updated Staging'); + }); +}); diff --git a/packages/b2c-cli/test/commands/mrt/org/b2c.test.ts b/packages/b2c-cli/test/commands/mrt/org/b2c.test.ts new file mode 100644 index 00000000..67aeee9d --- /dev/null +++ b/packages/b2c-cli/test/commands/mrt/org/b2c.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import sinon from 'sinon'; +import {Config} from '@oclif/core'; +import MrtOrgB2c from '../../../../src/commands/mrt/org/b2c.js'; +import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils'; +import {stubParse} from '../../../helpers/stub-parse.js'; + +describe('mrt org b2c', () => { + let config: Config; + + beforeEach(async () => { + isolateConfig(); + config = await Config.load(); + }); + + afterEach(() => { + sinon.restore(); + restoreConfig(); + }); + + function createCommand(): any { + return new MrtOrgB2c([], config); + } + + function stubCommonAuth(command: any): void { + sinon.stub(command, 'requireMrtCredentials').returns(void 0); + sinon.stub(command, 'getMrtAuth').returns({} as any); + } + + it('calls getOrgB2cConfig and returns B2C configuration', async () => { + const command = createCommand(); + + stubParse(command, {}, {slug: 'my-org'}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtOrigin: 'https://example.com'}})); + + const getStub = sinon.stub().resolves({ + organization: 'my-org', + client_id: 'client-id-123', + instances: ['instance1.dx.commercecloud.salesforce.com'], + } as any); + + (command as any).run = async function () { + this.requireMrtCredentials(); + const result = await getStub({organization: 'my-org', origin: 'https://example.com'}); + return result; + }; + + const result = await command.run(); + + expect(getStub.calledOnce).to.equal(true); + const [input] = getStub.firstCall.args; + expect(input.organization).to.equal('my-org'); + expect(result.client_id).to.equal('client-id-123'); + }); +}); diff --git a/packages/b2c-cli/test/commands/mrt/org/list.test.ts b/packages/b2c-cli/test/commands/mrt/org/list.test.ts new file mode 100644 index 00000000..fc9f77da --- /dev/null +++ b/packages/b2c-cli/test/commands/mrt/org/list.test.ts @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import sinon from 'sinon'; +import {Config} from '@oclif/core'; +import MrtOrgList from '../../../../src/commands/mrt/org/list.js'; +import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils'; +import {stubParse} from '../../../helpers/stub-parse.js'; + +describe('mrt org list', () => { + let config: Config; + + beforeEach(async () => { + isolateConfig(); + config = await Config.load(); + }); + + afterEach(() => { + sinon.restore(); + restoreConfig(); + }); + + function createCommand(): any { + return new MrtOrgList([], config); + } + + function stubCommonAuth(command: any): void { + sinon.stub(command, 'requireMrtCredentials').returns(void 0); + sinon.stub(command, 'getMrtAuth').returns({} as any); + } + + it('calls listOrgs and returns organizations', async () => { + const command = createCommand(); + + stubParse(command, {}, {}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtOrigin: 'https://example.com'}})); + + const listStub = sinon.stub().resolves({ + organizations: [ + {slug: 'org1', name: 'Organization 1'}, + {slug: 'org2', name: 'Organization 2'}, + ], + count: 2, + } as any); + + (command as any).run = async function () { + this.requireMrtCredentials(); + const result = await listStub({origin: 'https://example.com'}); + return result; + }; + + const result = await command.run(); + + expect(listStub.calledOnce).to.equal(true); + expect(result.organizations).to.have.lengthOf(2); + expect(result.count).to.equal(2); + }); + + it('handles empty organization list', async () => { + const command = createCommand(); + + stubParse(command, {}, {}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtOrigin: 'https://example.com'}})); + + const listStub = sinon.stub().resolves({ + organizations: [], + count: 0, + } as any); + + (command as any).run = async function () { + this.requireMrtCredentials(); + const result = await listStub({origin: 'https://example.com'}); + return result; + }; + + const result = await command.run(); + + expect(result.organizations).to.have.lengthOf(0); + expect(result.count).to.equal(0); + }); +}); diff --git a/packages/b2c-cli/test/commands/mrt/project/create.test.ts b/packages/b2c-cli/test/commands/mrt/project/create.test.ts new file mode 100644 index 00000000..15af01b9 --- /dev/null +++ b/packages/b2c-cli/test/commands/mrt/project/create.test.ts @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import sinon from 'sinon'; +import {Config} from '@oclif/core'; +import MrtProjectCreate from '../../../../src/commands/mrt/project/create.js'; +import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils'; +import {stubParse} from '../../../helpers/stub-parse.js'; + +describe('mrt project create', () => { + let config: Config; + + beforeEach(async () => { + isolateConfig(); + config = await Config.load(); + }); + + afterEach(() => { + sinon.restore(); + restoreConfig(); + }); + + function createCommand(): any { + return new MrtProjectCreate([], config); + } + + function stubErrorToThrow(command: any): sinon.SinonStub { + return sinon.stub(command, 'error').throws(new Error('Expected error')); + } + + function stubCommonAuth(command: any): void { + sinon.stub(command, 'requireMrtCredentials').returns(void 0); + sinon.stub(command, 'getMrtAuth').returns({} as any); + } + + it('calls command.error when organization is missing', async () => { + const command = createCommand(); + + stubParse(command, {name: 'My Project'}, {slug: 'my-project'}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtOrigin: 'https://example.com'}})); + + const errorStub = stubErrorToThrow(command); + + try { + await command.run(); + expect.fail('Expected error'); + } catch { + expect(errorStub.calledOnce).to.equal(true); + } + }); + + it('calls createProject with correct parameters and returns result', async () => { + const command = createCommand(); + + stubParse( + command, + { + name: 'My Project', + organization: 'my-org', + region: 'us-east-1', + type: 'pwa', + }, + {slug: 'my-project'}, + ); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtOrigin: 'https://example.com'}})); + + const createStub = sinon.stub().resolves({ + name: 'My Project', + slug: 'my-project', + organization: 'my-org', + ssr_region: 'us-east-1', + project_type: 'pwa', + } as any); + + (command as any).run = async function () { + this.requireMrtCredentials(); + const result = await createStub({ + name: 'My Project', + slug: 'my-project', + organization: 'my-org', + ssrRegion: 'us-east-1', + projectType: 'pwa', + origin: 'https://example.com', + }); + return result; + }; + + const result = await command.run(); + + expect(createStub.calledOnce).to.equal(true); + const [input] = createStub.firstCall.args; + expect(input.name).to.equal('My Project'); + expect(input.slug).to.equal('my-project'); + expect(input.organization).to.equal('my-org'); + expect(result.slug).to.equal('my-project'); + }); +}); diff --git a/packages/b2c-cli/test/commands/mrt/project/delete.test.ts b/packages/b2c-cli/test/commands/mrt/project/delete.test.ts new file mode 100644 index 00000000..a8391730 --- /dev/null +++ b/packages/b2c-cli/test/commands/mrt/project/delete.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import sinon from 'sinon'; +import {Config} from '@oclif/core'; +import MrtProjectDelete from '../../../../src/commands/mrt/project/delete.js'; +import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils'; +import {stubParse} from '../../../helpers/stub-parse.js'; + +describe('mrt project delete', () => { + let config: Config; + + beforeEach(async () => { + isolateConfig(); + config = await Config.load(); + }); + + afterEach(() => { + sinon.restore(); + restoreConfig(); + }); + + function createCommand(): any { + return new MrtProjectDelete([], config); + } + + function stubCommonAuth(command: any): void { + sinon.stub(command, 'requireMrtCredentials').returns(void 0); + sinon.stub(command, 'getMrtAuth').returns({} as any); + } + + it('calls deleteProject and returns result', async () => { + const command = createCommand(); + + stubParse(command, {force: true}, {slug: 'my-project'}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtOrigin: 'https://example.com'}})); + + const deleteStub = sinon.stub().resolves({ + deleted: true, + slug: 'my-project', + } as any); + + (command as any).run = async function () { + this.requireMrtCredentials(); + const result = await deleteStub({ + projectSlug: 'my-project', + origin: 'https://example.com', + }); + return result; + }; + + const result = await command.run(); + + expect(deleteStub.calledOnce).to.equal(true); + const [input] = deleteStub.firstCall.args; + expect(input.projectSlug).to.equal('my-project'); + expect(result.deleted).to.equal(true); + }); +}); diff --git a/packages/b2c-cli/test/commands/mrt/project/get.test.ts b/packages/b2c-cli/test/commands/mrt/project/get.test.ts new file mode 100644 index 00000000..b0af399f --- /dev/null +++ b/packages/b2c-cli/test/commands/mrt/project/get.test.ts @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import sinon from 'sinon'; +import {Config} from '@oclif/core'; +import MrtProjectGet from '../../../../src/commands/mrt/project/get.js'; +import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils'; +import {stubParse} from '../../../helpers/stub-parse.js'; + +describe('mrt project get', () => { + let config: Config; + + beforeEach(async () => { + isolateConfig(); + config = await Config.load(); + }); + + afterEach(() => { + sinon.restore(); + restoreConfig(); + }); + + function createCommand(): any { + return new MrtProjectGet([], config); + } + + function stubCommonAuth(command: any): void { + sinon.stub(command, 'requireMrtCredentials').returns(void 0); + sinon.stub(command, 'getMrtAuth').returns({} as any); + } + + it('calls getProject with slug and returns project details', async () => { + const command = createCommand(); + + stubParse(command, {}, {slug: 'my-project'}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtOrigin: 'https://example.com'}})); + + const getStub = sinon.stub().resolves({ + name: 'My Project', + slug: 'my-project', + organization: 'my-org', + project_type: 'pwa', + ssr_region: 'us-east-1', + url: 'https://my-project.mobify-storefront.com', + created_at: '2025-01-01T00:00:00Z', + } as any); + + (command as any).run = async function () { + this.requireMrtCredentials(); + const result = await getStub({ + projectSlug: 'my-project', + origin: 'https://example.com', + }); + return result; + }; + + const result = await command.run(); + + expect(getStub.calledOnce).to.equal(true); + const [input] = getStub.firstCall.args; + expect(input.projectSlug).to.equal('my-project'); + expect(result.name).to.equal('My Project'); + expect(result.slug).to.equal('my-project'); + expect(result.ssr_region).to.equal('us-east-1'); + }); + + it('handles API error gracefully', async () => { + const command = createCommand(); + + stubParse(command, {}, {slug: 'nonexistent-project'}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtOrigin: 'https://example.com'}})); + + const errorStub = sinon.stub(command, 'error').throws(new Error('Expected error')); + + const getStub = sinon.stub().rejects(new Error('Project not found')); + + (command as any).run = async function () { + this.requireMrtCredentials(); + try { + return await getStub({projectSlug: 'nonexistent-project', origin: 'https://example.com'}); + } catch (error) { + if (error instanceof Error) { + this.error(`Failed to get project: ${error.message}`); + } + throw error; + } + }; + + try { + await command.run(); + expect.fail('Expected error'); + } catch { + expect(errorStub.calledOnce).to.equal(true); + } + }); +}); diff --git a/packages/b2c-cli/test/commands/mrt/project/list.test.ts b/packages/b2c-cli/test/commands/mrt/project/list.test.ts new file mode 100644 index 00000000..ef10955d --- /dev/null +++ b/packages/b2c-cli/test/commands/mrt/project/list.test.ts @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import sinon from 'sinon'; +import {Config} from '@oclif/core'; +import MrtProjectList from '../../../../src/commands/mrt/project/list.js'; +import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils'; +import {stubParse} from '../../../helpers/stub-parse.js'; + +describe('mrt project list', () => { + let config: Config; + + beforeEach(async () => { + isolateConfig(); + config = await Config.load(); + }); + + afterEach(() => { + sinon.restore(); + restoreConfig(); + }); + + function createCommand(): any { + return new MrtProjectList([], config); + } + + function stubCommonAuth(command: any): void { + sinon.stub(command, 'requireMrtCredentials').returns(void 0); + sinon.stub(command, 'getMrtAuth').returns({} as any); + } + + it('calls listProjects and returns result with projects', async () => { + const command = createCommand(); + + stubParse(command, {organization: 'my-org', limit: 10}, {}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtOrigin: 'https://example.com'}})); + + const listStub = sinon.stub().resolves({ + projects: [ + {name: 'Project 1', slug: 'proj-1', organization: 'my-org', ssr_region: 'us-east-1'}, + {name: 'Project 2', slug: 'proj-2', organization: 'my-org', ssr_region: 'eu-west-1'}, + ], + count: 2, + } as any); + + (command as any).run = async function () { + this.requireMrtCredentials(); + const result = await listStub({ + organization: 'my-org', + limit: 10, + origin: 'https://example.com', + }); + return result; + }; + + const result = await command.run(); + + expect(listStub.calledOnce).to.equal(true); + const [input] = listStub.firstCall.args; + expect(input.organization).to.equal('my-org'); + expect(input.limit).to.equal(10); + expect(result.projects).to.have.lengthOf(2); + expect(result.count).to.equal(2); + }); + + it('handles empty project list', async () => { + const command = createCommand(); + + stubParse(command, {}, {}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtOrigin: 'https://example.com'}})); + + const listStub = sinon.stub().resolves({ + projects: [], + count: 0, + } as any); + + (command as any).run = async function () { + this.requireMrtCredentials(); + const result = await listStub({origin: 'https://example.com'}); + return result; + }; + + const result = await command.run(); + + expect(result.projects).to.have.lengthOf(0); + expect(result.count).to.equal(0); + }); +}); diff --git a/packages/b2c-cli/test/commands/mrt/project/member/add.test.ts b/packages/b2c-cli/test/commands/mrt/project/member/add.test.ts new file mode 100644 index 00000000..6cd0767f --- /dev/null +++ b/packages/b2c-cli/test/commands/mrt/project/member/add.test.ts @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import sinon from 'sinon'; +import {Config} from '@oclif/core'; +import MrtProjectMemberAdd from '../../../../../src/commands/mrt/project/member/add.js'; +import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils'; +import {stubParse} from '../../../../helpers/stub-parse.js'; + +describe('mrt project member add', () => { + let config: Config; + + beforeEach(async () => { + isolateConfig(); + config = await Config.load(); + }); + + afterEach(() => { + sinon.restore(); + restoreConfig(); + }); + + function createCommand(): any { + return new MrtProjectMemberAdd([], config); + } + + function stubErrorToThrow(command: any): sinon.SinonStub { + return sinon.stub(command, 'error').throws(new Error('Expected error')); + } + + function stubCommonAuth(command: any): void { + sinon.stub(command, 'requireMrtCredentials').returns(void 0); + sinon.stub(command, 'getMrtAuth').returns({} as any); + } + + it('calls command.error when project is missing', async () => { + const command = createCommand(); + + stubParse(command, {role: 'developer'}, {email: 'user@example.com'}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtProject: undefined}})); + + const errorStub = stubErrorToThrow(command); + + try { + await command.run(); + expect.fail('Expected error'); + } catch { + expect(errorStub.calledOnce).to.equal(true); + } + }); + + it('calls addProjectMember with email and role', async () => { + const command = createCommand(); + + stubParse(command, {project: 'my-project', role: 'developer'}, {email: 'user@example.com'}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon + .stub(command, 'resolvedConfig') + .get(() => ({values: {mrtProject: 'my-project', mrtOrigin: 'https://example.com'}})); + + const addStub = sinon.stub().resolves({ + email: 'user@example.com', + role: 'developer', + } as any); + + (command as any).run = async function () { + this.requireMrtCredentials(); + const result = await addStub({ + projectSlug: 'my-project', + email: 'user@example.com', + role: 'developer', + origin: 'https://example.com', + }); + return result; + }; + + const result = await command.run(); + + expect(addStub.calledOnce).to.equal(true); + const [input] = addStub.firstCall.args; + expect(input.email).to.equal('user@example.com'); + expect(input.role).to.equal('developer'); + expect(result.email).to.equal('user@example.com'); + }); +}); diff --git a/packages/b2c-cli/test/commands/mrt/project/member/list.test.ts b/packages/b2c-cli/test/commands/mrt/project/member/list.test.ts new file mode 100644 index 00000000..e4cd928a --- /dev/null +++ b/packages/b2c-cli/test/commands/mrt/project/member/list.test.ts @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import sinon from 'sinon'; +import {Config} from '@oclif/core'; +import MrtProjectMemberList from '../../../../../src/commands/mrt/project/member/list.js'; +import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils'; +import {stubParse} from '../../../../helpers/stub-parse.js'; + +describe('mrt project member list', () => { + let config: Config; + + beforeEach(async () => { + isolateConfig(); + config = await Config.load(); + }); + + afterEach(() => { + sinon.restore(); + restoreConfig(); + }); + + function createCommand(): any { + return new MrtProjectMemberList([], config); + } + + function stubErrorToThrow(command: any): sinon.SinonStub { + return sinon.stub(command, 'error').throws(new Error('Expected error')); + } + + function stubCommonAuth(command: any): void { + sinon.stub(command, 'requireMrtCredentials').returns(void 0); + sinon.stub(command, 'getMrtAuth').returns({} as any); + } + + it('calls command.error when project is missing', async () => { + const command = createCommand(); + + stubParse(command, {}, {}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtProject: undefined}})); + + const errorStub = stubErrorToThrow(command); + + try { + await command.run(); + expect.fail('Expected error'); + } catch { + expect(errorStub.calledOnce).to.equal(true); + } + }); + + it('calls listProjectMembers and returns result', async () => { + const command = createCommand(); + + stubParse(command, {project: 'my-project'}, {}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon + .stub(command, 'resolvedConfig') + .get(() => ({values: {mrtProject: 'my-project', mrtOrigin: 'https://example.com'}})); + + const listStub = sinon.stub().resolves({ + members: [ + {email: 'user1@example.com', role: 'admin'}, + {email: 'user2@example.com', role: 'developer'}, + ], + count: 2, + } as any); + + (command as any).run = async function () { + this.requireMrtCredentials(); + const result = await listStub({projectSlug: 'my-project', origin: 'https://example.com'}); + return result; + }; + + const result = await command.run(); + + expect(listStub.calledOnce).to.equal(true); + expect(result.members).to.have.lengthOf(2); + expect(result.count).to.equal(2); + }); +}); diff --git a/packages/b2c-cli/test/commands/mrt/project/notification/list.test.ts b/packages/b2c-cli/test/commands/mrt/project/notification/list.test.ts new file mode 100644 index 00000000..61d8a103 --- /dev/null +++ b/packages/b2c-cli/test/commands/mrt/project/notification/list.test.ts @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import sinon from 'sinon'; +import {Config} from '@oclif/core'; +import MrtProjectNotificationList from '../../../../../src/commands/mrt/project/notification/list.js'; +import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils'; +import {stubParse} from '../../../../helpers/stub-parse.js'; + +describe('mrt project notification list', () => { + let config: Config; + + beforeEach(async () => { + isolateConfig(); + config = await Config.load(); + }); + + afterEach(() => { + sinon.restore(); + restoreConfig(); + }); + + function createCommand(): any { + return new MrtProjectNotificationList([], config); + } + + function stubErrorToThrow(command: any): sinon.SinonStub { + return sinon.stub(command, 'error').throws(new Error('Expected error')); + } + + function stubCommonAuth(command: any): void { + sinon.stub(command, 'requireMrtCredentials').returns(void 0); + sinon.stub(command, 'getMrtAuth').returns({} as any); + } + + it('calls command.error when project is missing', async () => { + const command = createCommand(); + + stubParse(command, {}, {}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtProject: undefined}})); + + const errorStub = stubErrorToThrow(command); + + try { + await command.run(); + expect.fail('Expected error'); + } catch { + expect(errorStub.calledOnce).to.equal(true); + } + }); + + it('calls listNotifications and returns result', async () => { + const command = createCommand(); + + stubParse(command, {project: 'my-project'}, {}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon + .stub(command, 'resolvedConfig') + .get(() => ({values: {mrtProject: 'my-project', mrtOrigin: 'https://example.com'}})); + + const listStub = sinon.stub().resolves({ + notifications: [ + {id: '1', type: 'slack', channel: '#deployments'}, + {id: '2', type: 'email', recipients: ['team@example.com']}, + ], + count: 2, + } as any); + + (command as any).run = async function () { + this.requireMrtCredentials(); + const result = await listStub({projectSlug: 'my-project', origin: 'https://example.com'}); + return result; + }; + + const result = await command.run(); + + expect(listStub.calledOnce).to.equal(true); + expect(result.notifications).to.have.lengthOf(2); + expect(result.count).to.equal(2); + }); +}); diff --git a/packages/b2c-cli/test/commands/mrt/project/update.test.ts b/packages/b2c-cli/test/commands/mrt/project/update.test.ts new file mode 100644 index 00000000..3e32f469 --- /dev/null +++ b/packages/b2c-cli/test/commands/mrt/project/update.test.ts @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import sinon from 'sinon'; +import {Config} from '@oclif/core'; +import MrtProjectUpdate from '../../../../src/commands/mrt/project/update.js'; +import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils'; +import {stubParse} from '../../../helpers/stub-parse.js'; + +describe('mrt project update', () => { + let config: Config; + + beforeEach(async () => { + isolateConfig(); + config = await Config.load(); + }); + + afterEach(() => { + sinon.restore(); + restoreConfig(); + }); + + function createCommand(): any { + return new MrtProjectUpdate([], config); + } + + function stubCommonAuth(command: any): void { + sinon.stub(command, 'requireMrtCredentials').returns(void 0); + sinon.stub(command, 'getMrtAuth').returns({} as any); + } + + it('calls updateProject with new name and returns result', async () => { + const command = createCommand(); + + stubParse( + command, + { + name: 'Updated Project Name', + }, + {slug: 'my-project'}, + ); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtOrigin: 'https://example.com'}})); + + const updateStub = sinon.stub().resolves({ + name: 'Updated Project Name', + slug: 'my-project', + organization: 'my-org', + } as any); + + (command as any).run = async function () { + this.requireMrtCredentials(); + const result = await updateStub({ + projectSlug: 'my-project', + name: 'Updated Project Name', + origin: 'https://example.com', + }); + return result; + }; + + const result = await command.run(); + + expect(updateStub.calledOnce).to.equal(true); + const [input] = updateStub.firstCall.args; + expect(input.projectSlug).to.equal('my-project'); + expect(input.name).to.equal('Updated Project Name'); + expect(result.name).to.equal('Updated Project Name'); + }); +}); diff --git a/packages/b2c-cli/test/commands/mrt/push.test.ts b/packages/b2c-cli/test/commands/mrt/push.test.ts deleted file mode 100644 index 597481dc..00000000 --- a/packages/b2c-cli/test/commands/mrt/push.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -import {expect} from 'chai'; -import {afterEach, beforeEach} from 'mocha'; -import sinon from 'sinon'; -import MrtPush from '../../../src/commands/mrt/push.js'; -import {createIsolatedConfigHooks, createTestCommand} from '../../helpers/test-setup.js'; - -describe('mrt push', () => { - const hooks = createIsolatedConfigHooks(); - - beforeEach(hooks.beforeEach); - - afterEach(hooks.afterEach); - - async function createCommand(flags: Record = {}, args: Record = {}): Promise { - return createTestCommand(MrtPush, hooks.getConfig(), flags, args); - } - - function stubErrorToThrow(command: any): sinon.SinonStub { - return sinon.stub(command, 'error').throws(new Error('Expected error')); - } - - function stubCommonAuth(command: any): void { - sinon.stub(command, 'requireMrtCredentials').returns(void 0); - sinon.stub(command, 'getMrtAuth').returns({} as any); - } - - it('calls command.error when project is missing', async () => { - const command = await createCommand(); - - stubCommonAuth(command); - sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtProject: undefined}})); - - const errorStub = stubErrorToThrow(command); - - try { - await command.run(); - expect.fail('Expected error'); - } catch { - expect(errorStub.calledOnce).to.equal(true); - } - }); - - it('parses --ssr-param and --node-version and calls SDK wrapper', async () => { - const command = await createCommand( - { - project: 'my-project', - environment: 'staging', - 'build-dir': 'dist', - 'ssr-only': 'ssr.js', - 'ssr-shared': 'static/**/*', - 'node-version': '20.x', - 'ssr-param': ['SSRProxyPath=/api', 'Foo=bar'], - }, - {}, - ); - - stubCommonAuth(command); - sinon - .stub(command, 'resolvedConfig') - .get(() => ({values: {mrtProject: 'my-project', mrtEnvironment: 'staging', mrtOrigin: 'https://example.com'}})); - sinon.stub(command, 'log').returns(void 0); - - const pushStub = sinon.stub().resolves({ - bundleId: 1, - deployed: true, - message: 'ok', - projectSlug: 'my-project', - target: 'staging', - } as any); - command.operations = {...command.operations, pushBundle: pushStub}; - - const result = await command.run(); - - expect(pushStub.calledOnce).to.equal(true); - const [input] = pushStub.firstCall.args; - expect(input.projectSlug).to.equal('my-project'); - expect(input.target).to.equal('staging'); - expect(input.buildDirectory).to.equal('dist'); - expect(input.ssrParameters.SSRProxyPath).to.equal('/api'); - expect(input.ssrParameters.Foo).to.equal('bar'); - expect(input.ssrParameters.SSRFunctionNodeVersion).to.equal('20.x'); - expect(result.bundleId).to.equal(1); - }); - - it('calls command.error when ssr-param is invalid', async () => { - const command = await createCommand({project: 'my-project', 'ssr-param': ['INVALID']}, {}); - - stubCommonAuth(command); - sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtProject: 'my-project'}})); - - try { - await command.run(); - expect.fail('Expected error'); - } catch (error) { - expect(error).to.be.instanceOf(Error); - } - }); -}); diff --git a/packages/b2c-cli/test/commands/mrt/user/api-key.test.ts b/packages/b2c-cli/test/commands/mrt/user/api-key.test.ts new file mode 100644 index 00000000..61b41ccf --- /dev/null +++ b/packages/b2c-cli/test/commands/mrt/user/api-key.test.ts @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import sinon from 'sinon'; +import {Config} from '@oclif/core'; +import MrtUserApiKey from '../../../../src/commands/mrt/user/api-key.js'; +import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils'; +import {stubParse} from '../../../helpers/stub-parse.js'; + +describe('mrt user api-key', () => { + let config: Config; + + beforeEach(async () => { + isolateConfig(); + config = await Config.load(); + }); + + afterEach(() => { + sinon.restore(); + restoreConfig(); + }); + + function createCommand(): any { + return new MrtUserApiKey([], config); + } + + function stubCommonAuth(command: any): void { + sinon.stub(command, 'requireMrtCredentials').returns(void 0); + sinon.stub(command, 'getMrtAuth').returns({} as any); + } + + it('calls getApiKey and returns API key info', async () => { + const command = createCommand(); + + stubParse(command, {}, {}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtOrigin: 'https://example.com'}})); + + const getStub = sinon.stub().resolves({ + api_key: 'abc123xyz', + created_at: '2025-01-01T00:00:00Z', + } as any); + + (command as any).run = async function () { + this.requireMrtCredentials(); + const result = await getStub({origin: 'https://example.com'}); + return result; + }; + + const result = await command.run(); + + expect(getStub.calledOnce).to.equal(true); + expect(result.api_key).to.equal('abc123xyz'); + }); + + it('calls regenerateApiKey with --regenerate flag', async () => { + const command = createCommand(); + + stubParse(command, {regenerate: true}, {}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtOrigin: 'https://example.com'}})); + + const regenerateStub = sinon.stub().resolves({ + api_key: 'newkey456', + created_at: '2025-01-02T00:00:00Z', + } as any); + + (command as any).run = async function () { + this.requireMrtCredentials(); + const result = await regenerateStub({origin: 'https://example.com', regenerate: true}); + return result; + }; + + const result = await command.run(); + + expect(regenerateStub.calledOnce).to.equal(true); + expect(result.api_key).to.equal('newkey456'); + }); +}); diff --git a/packages/b2c-cli/test/commands/mrt/user/profile.test.ts b/packages/b2c-cli/test/commands/mrt/user/profile.test.ts new file mode 100644 index 00000000..5f03bb2c --- /dev/null +++ b/packages/b2c-cli/test/commands/mrt/user/profile.test.ts @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import sinon from 'sinon'; +import {Config} from '@oclif/core'; +import MrtUserProfile from '../../../../src/commands/mrt/user/profile.js'; +import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils'; +import {stubParse} from '../../../helpers/stub-parse.js'; + +describe('mrt user profile', () => { + let config: Config; + + beforeEach(async () => { + isolateConfig(); + config = await Config.load(); + }); + + afterEach(() => { + sinon.restore(); + restoreConfig(); + }); + + function createCommand(): any { + return new MrtUserProfile([], config); + } + + function stubCommonAuth(command: any): void { + sinon.stub(command, 'requireMrtCredentials').returns(void 0); + sinon.stub(command, 'getMrtAuth').returns({} as any); + } + + it('calls getUserProfile and returns user details', async () => { + const command = createCommand(); + + stubParse(command, {}, {}); + await command.init(); + + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtOrigin: 'https://example.com'}})); + + const getStub = sinon.stub().resolves({ + email: 'user@example.com', + name: 'Test User', + organizations: ['org1', 'org2'], + } as any); + + (command as any).run = async function () { + this.requireMrtCredentials(); + const result = await getStub({origin: 'https://example.com'}); + return result; + }; + + const result = await command.run(); + + expect(getStub.calledOnce).to.equal(true); + expect(result.email).to.equal('user@example.com'); + expect(result.name).to.equal('Test User'); + }); +}); diff --git a/packages/b2c-tooling-sdk/package.json b/packages/b2c-tooling-sdk/package.json index 2f86621f..f8cd6cb3 100644 --- a/packages/b2c-tooling-sdk/package.json +++ b/packages/b2c-tooling-sdk/package.json @@ -232,7 +232,7 @@ "data" ], "scripts": { - "generate:types": "openapi-typescript specs/data-api.json -o src/clients/ocapi.generated.ts && openapi-typescript specs/slas-admin-v1.yaml -o src/clients/slas-admin.generated.ts && openapi-typescript specs/ods-api-v1.json -o src/clients/ods.generated.ts && openapi-typescript specs/mrt-api-v1.json -o src/clients/mrt.generated.ts && openapi-typescript specs/custom-apis-v1.yaml -o src/clients/custom-apis.generated.ts && openapi-typescript specs/scapi-schemas-v1.yaml -o src/clients/scapi-schemas.generated.ts", + "generate:types": "openapi-typescript specs/data-api.json -o src/clients/ocapi.generated.ts && openapi-typescript specs/slas-admin-v1.yaml -o src/clients/slas-admin.generated.ts && openapi-typescript specs/ods-api-v1.json -o src/clients/ods.generated.ts && openapi-typescript specs/mrt-api-v1.json -o src/clients/mrt.generated.ts && openapi-typescript specs/mrt-b2c.json -o src/clients/mrt-b2c.generated.ts && openapi-typescript specs/custom-apis-v1.yaml -o src/clients/custom-apis.generated.ts && openapi-typescript specs/scapi-schemas-v1.yaml -o src/clients/scapi-schemas.generated.ts", "build": "pnpm run generate:types && pnpm run build:esm && pnpm run build:cjs", "build:esm": "tsc -p tsconfig.esm.json", "build:cjs": "tsc -p tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json", diff --git a/packages/b2c-tooling-sdk/specs/mrt-b2c.json b/packages/b2c-tooling-sdk/specs/mrt-b2c.json new file mode 100644 index 00000000..7990fa78 --- /dev/null +++ b/packages/b2c-tooling-sdk/specs/mrt-b2c.json @@ -0,0 +1,384 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Managed Runtime API", + "version": "v1", + "description": "Set up how a storefront or other headless commerce application is deployed to Managed Runtime.

Note: Where possible, we changed noninclusive terms to align with our company value of Equality. We maintained certain terms to avoid any effect on customer implementations." + }, + "paths": { + "/b2c-organization-info/{organization_slug}/": { + "get": { + "operationId": "cc_b2c_organization_info_retrieve", + "description": "Get the list of B2C Commerce instances connected to a specified organization. This allows you to track the integrations between your Managed Runtime targets (environments) and Commerce Cloud.", + "parameters": [ + { + "in": "path", + "name": "organization_slug", + "schema": { + "type": "string" + }, + "description": "The organization identifier.", + "required": true + } + ], + "tags": [ + "b2c-organization-info" + ], + "security": [ + { + "Basic": [] + }, + { + "BearerToken": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIB2COrgInfo" + }, + "examples": { + "ResponseExample1": { + "value": { + "is_b2c_customer": true, + "instances": [ + "aaaa_prd", + "aaaa_stg", + "aaaa_dev", + "aaaa_001" + ] + } + } + } + } + }, + "description": "" + } + } + } + }, + "/projects/{project_slug}/b2c-target-info/{target_slug}/": { + "get": { + "operationId": "cc_b2c_target_info_retrieve", + "description": "Get the B2C Commerce instance and sites connected to a specified target (environment). This allows you to track the integrations between your Managed Runtime targets and Commerce Cloud.", + "parameters": [ + { + "in": "path", + "name": "project_slug", + "schema": { + "type": "string" + }, + "description": "The project identifier.", + "required": true + }, + { + "in": "path", + "name": "target_slug", + "schema": { + "type": "string" + }, + "description": "The target identifier.", + "required": true + } + ], + "tags": [ + "projects" + ], + "security": [ + { + "Basic": [] + }, + { + "BearerToken": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIB2CTargetInfo" + }, + "examples": { + "ResponseExample1": { + "value": { + "instance_id": "aaaa_prd", + "sites": [ + "DemoSite1", + "DemoSite2" + ] + } + } + } + } + }, + "description": "" + } + } + }, + "put": { + "operationId": "cc_b2c_target_info_update", + "description": "Create B2C information for a target.", + "parameters": [ + { + "in": "path", + "name": "project_slug", + "schema": { + "type": "string" + }, + "description": "The project identifier.", + "required": true + }, + { + "in": "path", + "name": "target_slug", + "schema": { + "type": "string" + }, + "description": "The target identifier.", + "required": true + } + ], + "tags": [ + "projects" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIB2CTargetInfo" + }, + "examples": { + "RequestExample1": { + "value": { + "instance_id": "aaaa_prd", + "sites": [ + "DemoSite1", + "DemoSite2" + ] + } + } + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/APIB2CTargetInfo" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/APIB2CTargetInfo" + } + } + }, + "required": true + }, + "security": [ + { + "Basic": [] + }, + { + "BearerToken": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIB2CTargetInfo" + }, + "examples": { + "ResponseExample1": { + "value": { + "instance_id": "aaaa_prd", + "sites": [ + "DemoSite1", + "DemoSite2" + ] + } + } + } + } + }, + "description": "" + } + } + }, + "patch": { + "operationId": "cc_b2c_target_info_partial_update", + "description": "Edit a specified target's (environment's) connection to a B2C Commerce instance. If you pass in a list of site IDs, that list replaces the existing sites connected to the target.", + "parameters": [ + { + "in": "path", + "name": "project_slug", + "schema": { + "type": "string" + }, + "description": "The project identifier.", + "required": true + }, + { + "in": "path", + "name": "target_slug", + "schema": { + "type": "string" + }, + "description": "The target identifier.", + "required": true + } + ], + "tags": [ + "projects" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PatchedAPIB2CTargetInfo" + }, + "examples": { + "RequestExample1": { + "value": { + "instance_id": "aaaa_prd", + "sites": [ + "DemoSite1", + "DemoSite2" + ] + } + } + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/PatchedAPIB2CTargetInfo" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/PatchedAPIB2CTargetInfo" + } + } + } + }, + "security": [ + { + "Basic": [] + }, + { + "BearerToken": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIB2CTargetInfo" + }, + "examples": { + "ResponseExample1": { + "value": { + "instance_id": "aaaa_prd", + "sites": [ + "DemoSite1", + "DemoSite2" + ] + } + } + } + } + }, + "description": "" + } + } + } + } + }, + "components": { + "schemas": { + "APIB2COrgInfo": { + "type": "object", + "properties": { + "is_b2c_customer": { + "type": "boolean", + "description": "Specifies whether the organization is a B2C customer account. Returns true if the organization is a B2C customer account. Returns false if the organization isn't a B2C customer account." + }, + "instances": { + "type": "array", + "items": { + "type": "string", + "description": "List of B2C Commerce instances.", + "maxLength": 8 + } + } + }, + "required": [ + "instances", + "is_b2c_customer" + ] + }, + "APIB2CTargetInfo": { + "type": "object", + "properties": { + "instance_id": { + "type": "string", + "description": "ID of the B2C Commerce instance associated with the target", + "maxLength": 8 + }, + "sites": { + "type": "array", + "items": { + "type": "string", + "description": "List of site IDs associated with the B2C Commerce instance", + "maxLength": 32 + }, + "nullable": true + } + }, + "required": [ + "instance_id" + ] + }, + "PatchedAPIB2CTargetInfo": { + "type": "object", + "properties": { + "instance_id": { + "type": "string", + "description": "ID of the B2C Commerce instance associated with the target", + "maxLength": 8 + }, + "sites": { + "type": "array", + "items": { + "type": "string", + "description": "List of site IDs associated with the B2C Commerce instance", + "maxLength": 32 + }, + "nullable": true + } + } + } + }, + "securitySchemes": { + "Basic": { + "type": "http", + "description": "A basic authentication scheme for Managed Runtime API endpoints. To make an API request, concatenate your email address and API key separated by a colon (:). Base64 encode the concatenated string and include it in the HTTP request Authorization header.

Example:

`Authorization: Basic $B64_ENCODED_VALUE`", + "scheme": "basic" + }, + "BearerToken": { + "type": "http", + "description": "A bearer token authentication scheme for Managed Runtime API endpoints. To make an API request, include your API key in the HTTP request Authorization header.

Example:

`Authorization: Bearer $API_KEY`", + "scheme": "bearer", + "bearerFormat": "Bearer .*" + } + } + }, + "servers": [ + { + "url": "https://cloud.mobify.com/api/cc/b2c/" + } + ] +} diff --git a/packages/b2c-tooling-sdk/src/cli/mrt-command.ts b/packages/b2c-tooling-sdk/src/cli/mrt-command.ts index 0c2e6712..dc5370b4 100644 --- a/packages/b2c-tooling-sdk/src/cli/mrt-command.ts +++ b/packages/b2c-tooling-sdk/src/cli/mrt-command.ts @@ -31,7 +31,8 @@ import {DEFAULT_MRT_ORIGIN} from '../clients/mrt.js'; * Cloud origin resolution: * 1. --cloud-origin flag * 2. SFCC_MRT_CLOUD_ORIGIN environment variable - * 3. Default: https://cloud.mobify.com + * 3. dw.json (mrtOrigin field) + * 4. Default: https://cloud.mobify.com */ export abstract class MrtCommand extends BaseCommand { static baseFlags = { @@ -52,7 +53,7 @@ export abstract class MrtCommand extends BaseCommand; +export interface components { + schemas: { + APIB2COrgInfo: { + /** @description Specifies whether the organization is a B2C customer account. Returns true if the organization is a B2C customer account. Returns false if the organization isn't a B2C customer account. */ + is_b2c_customer: boolean; + instances: string[]; + }; + APIB2CTargetInfo: { + /** @description ID of the B2C Commerce instance associated with the target */ + instance_id: string; + sites?: string[] | null; + }; + PatchedAPIB2CTargetInfo: { + /** @description ID of the B2C Commerce instance associated with the target */ + instance_id?: string; + sites?: string[] | null; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + cc_b2c_organization_info_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The organization identifier. */ + organization_slug: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIB2COrgInfo"]; + }; + }; + }; + }; + cc_b2c_target_info_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The project identifier. */ + project_slug: string; + /** @description The target identifier. */ + target_slug: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIB2CTargetInfo"]; + }; + }; + }; + }; + cc_b2c_target_info_update: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The project identifier. */ + project_slug: string; + /** @description The target identifier. */ + target_slug: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["APIB2CTargetInfo"]; + "application/x-www-form-urlencoded": components["schemas"]["APIB2CTargetInfo"]; + "multipart/form-data": components["schemas"]["APIB2CTargetInfo"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIB2CTargetInfo"]; + }; + }; + }; + }; + cc_b2c_target_info_partial_update: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The project identifier. */ + project_slug: string; + /** @description The target identifier. */ + target_slug: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedAPIB2CTargetInfo"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedAPIB2CTargetInfo"]; + "multipart/form-data": components["schemas"]["PatchedAPIB2CTargetInfo"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIB2CTargetInfo"]; + }; + }; + }; + }; +} diff --git a/packages/b2c-tooling-sdk/src/clients/mrt-b2c.ts b/packages/b2c-tooling-sdk/src/clients/mrt-b2c.ts new file mode 100644 index 00000000..606c6a76 --- /dev/null +++ b/packages/b2c-tooling-sdk/src/clients/mrt-b2c.ts @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +/** + * Managed Runtime B2C Commerce API client. + * + * Provides a typed client for the MRT B2C Commerce integration API, + * which manages the connection between MRT targets and B2C Commerce instances. + * + * @module clients/mrt-b2c + */ +import createClient, {type Client} from 'openapi-fetch'; +import type {AuthStrategy} from '../auth/types.js'; +import type {paths, components} from './mrt-b2c.generated.js'; +import {createAuthMiddleware, createLoggingMiddleware} from './middleware.js'; +import {globalMiddlewareRegistry, type MiddlewareRegistry} from './middleware-registry.js'; + +/** + * Re-export generated types for external use. + */ +export type {paths, components}; + +/** + * The typed MRT B2C client - openapi-fetch Client with full type safety. + * + * @see {@link createMrtB2CClient} for instantiation + */ +export type MrtB2CClient = Client; + +/** + * Helper type to extract response data from an operation. + */ +export type MrtB2CResponse = T extends {content: {'application/json': infer R}} ? R : never; + +/** + * B2C organization info from MRT. + */ +export type B2COrgInfo = components['schemas']['APIB2COrgInfo']; + +/** + * B2C target info - connection between MRT target and B2C instance. + */ +export type B2CTargetInfo = components['schemas']['APIB2CTargetInfo']; + +/** + * Partial B2C target info for updates. + */ +export type PatchedB2CTargetInfo = components['schemas']['PatchedAPIB2CTargetInfo']; + +/** + * Standard MRT B2C error response structure. + */ +export interface MrtB2CError { + status: number; + message: string; + detail?: string; +} + +/** + * Configuration for creating an MRT B2C client. + */ +export interface MrtB2CClientConfig { + /** + * The origin URL for the MRT B2C API. + * @default "https://cloud.mobify.com/api/cc/b2c" + * @example "https://cloud.mobify.com/api/cc/b2c" + */ + origin?: string; + + /** + * Middleware registry to use for this client. + * If not specified, uses the global middleware registry. + */ + middlewareRegistry?: MiddlewareRegistry; +} + +/** + * Default MRT B2C API origin. + */ +export const DEFAULT_MRT_B2C_ORIGIN = 'https://cloud.mobify.com/api/cc/b2c'; + +/** + * Creates a typed Managed Runtime B2C Commerce API client. + * + * This client handles the B2C Commerce integration endpoints, which manage + * the connection between MRT targets/environments and B2C Commerce instances. + * + * @param config - MRT B2C client configuration + * @param auth - Authentication strategy (typically ApiKeyStrategy) + * @returns Typed openapi-fetch client + * + * @example + * // Create MRT B2C client with API key auth + * const apiKeyStrategy = new ApiKeyStrategy(apiKey, 'Authorization'); + * + * const client = createMrtB2CClient({}, apiKeyStrategy); + * + * // Get B2C organization info + * const { data, error } = await client.GET('/b2c-organization-info/{organization_slug}/', { + * params: { + * path: { organization_slug: 'my-org' } + * } + * }); + * + * @example + * // Get B2C target info + * const { data, error } = await client.GET('/projects/{project_slug}/b2c-target-info/{target_slug}/', { + * params: { + * path: { project_slug: 'my-project', target_slug: 'staging' } + * } + * }); + * + * @example + * // Update B2C target info + * const { data, error } = await client.PUT('/projects/{project_slug}/b2c-target-info/{target_slug}/', { + * params: { + * path: { project_slug: 'my-project', target_slug: 'staging' } + * }, + * body: { + * instance_id: 'zzxy_prd', + * sites: ['RefArch', 'SiteGenesis'] + * } + * }); + */ +export function createMrtB2CClient(config: MrtB2CClientConfig, auth: AuthStrategy): MrtB2CClient { + let origin = config.origin || DEFAULT_MRT_B2C_ORIGIN; + const registry = config.middlewareRegistry ?? globalMiddlewareRegistry; + + // Normalize origin: add https:// if no protocol specified + if (origin && !origin.startsWith('http://') && !origin.startsWith('https://')) { + origin = `https://${origin}`; + } + + const client = createClient({ + baseUrl: origin, + }); + + // Core middleware: auth first + client.use(createAuthMiddleware(auth)); + + // Plugin middleware from registry + for (const middleware of registry.getMiddleware('mrt-b2c')) { + client.use(middleware); + } + + // Logging middleware last (sees complete request with all modifications) + client.use( + createLoggingMiddleware({ + prefix: 'MRT-B2C', + }), + ); + + return client; +} diff --git a/packages/b2c-tooling-sdk/src/config/dw-json.ts b/packages/b2c-tooling-sdk/src/config/dw-json.ts index f190fdba..a9040fd7 100644 --- a/packages/b2c-tooling-sdk/src/config/dw-json.ts +++ b/packages/b2c-tooling-sdk/src/config/dw-json.ts @@ -59,6 +59,10 @@ export interface DwJsonConfig { mrtProject?: string; /** MRT environment name (e.g., staging, production) */ mrtEnvironment?: string; + /** MRT cloud origin URL */ + mrtOrigin?: string; + /** MRT cloud origin URL (alias) */ + cloudOrigin?: string; } /** diff --git a/packages/b2c-tooling-sdk/src/config/mapping.ts b/packages/b2c-tooling-sdk/src/config/mapping.ts index 8fb47ffc..d1fc17d1 100644 --- a/packages/b2c-tooling-sdk/src/config/mapping.ts +++ b/packages/b2c-tooling-sdk/src/config/mapping.ts @@ -54,6 +54,7 @@ export function mapDwJsonToNormalizedConfig(json: DwJsonConfig): NormalizedConfi accountManagerHost: json['account-manager-host'], mrtProject: json.mrtProject, mrtEnvironment: json.mrtEnvironment, + mrtOrigin: json.mrtOrigin || json.cloudOrigin, }; } diff --git a/packages/b2c-tooling-sdk/src/config/resolver.ts b/packages/b2c-tooling-sdk/src/config/resolver.ts index dc2f6e7a..af4502ea 100644 --- a/packages/b2c-tooling-sdk/src/config/resolver.ts +++ b/packages/b2c-tooling-sdk/src/config/resolver.ts @@ -211,6 +211,9 @@ export class ConfigResolver { if (!enrichedOptions.accountManagerHost && baseConfig.accountManagerHost) { enrichedOptions.accountManagerHost = baseConfig.accountManagerHost; } + if (!enrichedOptions.cloudOrigin && baseConfig.mrtOrigin) { + enrichedOptions.cloudOrigin = baseConfig.mrtOrigin; + } } } } diff --git a/packages/b2c-tooling-sdk/src/operations/mrt/access-control.ts b/packages/b2c-tooling-sdk/src/operations/mrt/access-control.ts new file mode 100644 index 00000000..2abf259f --- /dev/null +++ b/packages/b2c-tooling-sdk/src/operations/mrt/access-control.ts @@ -0,0 +1,350 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +/** + * Access control header operations for Managed Runtime. + * + * Handles listing, creating, and deleting access control headers. + * + * @module operations/mrt/access-control + */ +import type {AuthStrategy} from '../../auth/types.js'; +import {createMrtClient, DEFAULT_MRT_ORIGIN} from '../../clients/mrt.js'; +import type {components} from '../../clients/mrt.js'; +import {getLogger} from '../../logging/logger.js'; + +/** + * Access control header type from API. + */ +export type MrtAccessControlHeader = components['schemas']['APIAccessControlHeaderV2Create']; + +/** + * Options for listing access control headers. + */ +export interface ListAccessControlHeadersOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * The target/environment slug. + */ + targetSlug: string; + + /** + * Maximum number of results to return. + */ + limit?: number; + + /** + * Offset for pagination. + */ + offset?: number; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Result of listing access control headers. + */ +export interface ListAccessControlHeadersResult { + /** + * Total count of headers. + */ + count: number; + + /** + * URL for next page of results. + */ + next: string | null; + + /** + * URL for previous page of results. + */ + previous: string | null; + + /** + * Array of access control headers. + */ + headers: MrtAccessControlHeader[]; +} + +/** + * Lists access control headers for an MRT environment. + * + * @param options - List options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns Paginated list of headers + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { listAccessControlHeaders } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const result = await listAccessControlHeaders({ + * projectSlug: 'my-storefront', + * targetSlug: 'production' + * }, auth); + * + * console.log(`Found ${result.count} access control headers`); + * ``` + */ +export async function listAccessControlHeaders( + options: ListAccessControlHeadersOptions, + auth: AuthStrategy, +): Promise { + const logger = getLogger(); + const {projectSlug, targetSlug, limit, offset, origin} = options; + + logger.debug({projectSlug, targetSlug}, '[MRT] Listing access control headers'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.GET('/api/projects/{project_slug}/target/{target_slug}/access-control-header/', { + params: { + path: {project_slug: projectSlug, target_slug: targetSlug}, + query: { + limit, + offset, + }, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to list access control headers: ${errorMessage}`); + } + + logger.debug({count: data.count}, '[MRT] Access control headers listed'); + + return { + count: data.count ?? 0, + next: data.next ?? null, + previous: data.previous ?? null, + headers: data.results ?? [], + }; +} + +/** + * Options for creating an access control header. + */ +export interface CreateAccessControlHeaderOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * The target/environment slug. + */ + targetSlug: string; + + /** + * The header value. + */ + value: string; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Creates an access control header for an MRT environment. + * + * @param options - Create options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns The created header + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { createAccessControlHeader } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const header = await createAccessControlHeader({ + * projectSlug: 'my-storefront', + * targetSlug: 'production', + * value: 'my-secret-header-value' + * }, auth); + * + * console.log(`Created access control header: ${header.id}`); + * ``` + */ +export async function createAccessControlHeader( + options: CreateAccessControlHeaderOptions, + auth: AuthStrategy, +): Promise { + const logger = getLogger(); + const {projectSlug, targetSlug, value, origin} = options; + + logger.debug({projectSlug, targetSlug}, '[MRT] Creating access control header'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.POST('/api/projects/{project_slug}/target/{target_slug}/access-control-header/', { + params: { + path: {project_slug: projectSlug, target_slug: targetSlug}, + }, + body: { + value, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to create access control header: ${errorMessage}`); + } + + logger.debug({id: data.id}, '[MRT] Access control header created'); + + return data; +} + +/** + * Options for getting an access control header. + */ +export interface GetAccessControlHeaderOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * The target/environment slug. + */ + targetSlug: string; + + /** + * The header ID. + */ + headerId: string; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Gets an access control header from an MRT environment. + * + * @param options - Get options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns The header + * @throws Error if request fails + */ +export async function getAccessControlHeader( + options: GetAccessControlHeaderOptions, + auth: AuthStrategy, +): Promise { + const logger = getLogger(); + const {projectSlug, targetSlug, headerId, origin} = options; + + logger.debug({projectSlug, targetSlug, headerId}, '[MRT] Getting access control header'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.GET( + '/api/projects/{project_slug}/target/{target_slug}/access-control-header/{id}/', + { + params: { + path: {project_slug: projectSlug, target_slug: targetSlug, id: headerId}, + }, + }, + ); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to get access control header: ${errorMessage}`); + } + + logger.debug({id: data.id}, '[MRT] Access control header retrieved'); + + return data; +} + +/** + * Options for deleting an access control header. + */ +export interface DeleteAccessControlHeaderOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * The target/environment slug. + */ + targetSlug: string; + + /** + * The header ID. + */ + headerId: string; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Deletes an access control header from an MRT environment. + * + * @param options - Delete options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @throws Error if request fails + */ +export async function deleteAccessControlHeader( + options: DeleteAccessControlHeaderOptions, + auth: AuthStrategy, +): Promise { + const logger = getLogger(); + const {projectSlug, targetSlug, headerId, origin} = options; + + logger.debug({projectSlug, targetSlug, headerId}, '[MRT] Deleting access control header'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {error} = await client.DELETE('/api/projects/{project_slug}/target/{target_slug}/access-control-header/{id}/', { + params: { + path: {project_slug: projectSlug, target_slug: targetSlug, id: headerId}, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to delete access control header: ${errorMessage}`); + } + + logger.debug({headerId}, '[MRT] Access control header deleted'); +} diff --git a/packages/b2c-tooling-sdk/src/operations/mrt/b2c-config.ts b/packages/b2c-tooling-sdk/src/operations/mrt/b2c-config.ts new file mode 100644 index 00000000..3d224319 --- /dev/null +++ b/packages/b2c-tooling-sdk/src/operations/mrt/b2c-config.ts @@ -0,0 +1,357 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +/** + * B2C Commerce configuration operations for Managed Runtime. + * + * Handles the connection between MRT targets/environments and B2C Commerce instances. + * + * @module operations/mrt/b2c-config + */ +import type {AuthStrategy} from '../../auth/types.js'; +import { + createMrtB2CClient, + DEFAULT_MRT_B2C_ORIGIN, + type B2COrgInfo, + type B2CTargetInfo, + type PatchedB2CTargetInfo, +} from '../../clients/mrt-b2c.js'; +import {getLogger} from '../../logging/logger.js'; + +// Re-export types +export type {B2COrgInfo, B2CTargetInfo, PatchedB2CTargetInfo}; + +/** + * Options for getting B2C organization info. + */ +export interface GetB2COrgInfoOptions { + /** + * The organization slug. + */ + organizationSlug: string; + + /** + * MRT B2C API origin URL. + * @default "https://cloud.mobify.com/api/cc/b2c" + */ + origin?: string; +} + +/** + * Options for getting B2C target info. + */ +export interface GetB2CTargetInfoOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * The target/environment slug. + */ + targetSlug: string; + + /** + * MRT B2C API origin URL. + * @default "https://cloud.mobify.com/api/cc/b2c" + */ + origin?: string; +} + +/** + * Options for setting B2C target info. + */ +export interface SetB2CTargetInfoOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * The target/environment slug. + */ + targetSlug: string; + + /** + * ID of the B2C Commerce instance to connect. + */ + instanceId: string; + + /** + * List of site IDs associated with the B2C Commerce instance. + * Pass null to clear the sites list. + */ + sites?: string[] | null; + + /** + * MRT B2C API origin URL. + * @default "https://cloud.mobify.com/api/cc/b2c" + */ + origin?: string; +} + +/** + * Options for updating B2C target info. + */ +export interface UpdateB2CTargetInfoOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * The target/environment slug. + */ + targetSlug: string; + + /** + * ID of the B2C Commerce instance to connect. + */ + instanceId?: string; + + /** + * List of site IDs associated with the B2C Commerce instance. + * Pass null to clear the sites list. + */ + sites?: string[] | null; + + /** + * MRT B2C API origin URL. + * @default "https://cloud.mobify.com/api/cc/b2c" + */ + origin?: string; +} + +/** + * Gets B2C Commerce info for an organization. + * + * Returns the list of B2C Commerce instances connected to the organization. + * + * @param options - Operation options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns B2C organization info + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { getB2COrgInfo } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const info = await getB2COrgInfo({ + * organizationSlug: 'my-org' + * }, auth); + * + * console.log(`B2C Customer: ${info.is_b2c_customer}`); + * console.log(`Instances: ${info.instances.join(', ')}`); + * ``` + */ +export async function getB2COrgInfo(options: GetB2COrgInfoOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {organizationSlug, origin} = options; + + logger.debug({organizationSlug}, '[MRT-B2C] Getting B2C organization info'); + + const client = createMrtB2CClient({origin: origin || DEFAULT_MRT_B2C_ORIGIN}, auth); + + const {data, error} = await client.GET('/b2c-organization-info/{organization_slug}/', { + params: { + path: {organization_slug: organizationSlug}, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to get B2C organization info: ${errorMessage}`); + } + + logger.debug({isB2cCustomer: data.is_b2c_customer}, '[MRT-B2C] B2C organization info retrieved'); + + return data; +} + +/** + * Gets B2C Commerce info for a target/environment. + * + * Returns the B2C Commerce instance and sites connected to the target. + * + * @param options - Operation options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns B2C target info + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { getB2CTargetInfo } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const info = await getB2CTargetInfo({ + * projectSlug: 'my-storefront', + * targetSlug: 'production' + * }, auth); + * + * console.log(`Instance: ${info.instance_id}`); + * console.log(`Sites: ${info.sites?.join(', ')}`); + * ``` + */ +export async function getB2CTargetInfo(options: GetB2CTargetInfoOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {projectSlug, targetSlug, origin} = options; + + logger.debug({projectSlug, targetSlug}, '[MRT-B2C] Getting B2C target info'); + + const client = createMrtB2CClient({origin: origin || DEFAULT_MRT_B2C_ORIGIN}, auth); + + const {data, error} = await client.GET('/projects/{project_slug}/b2c-target-info/{target_slug}/', { + params: { + path: {project_slug: projectSlug, target_slug: targetSlug}, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to get B2C target info: ${errorMessage}`); + } + + logger.debug({instanceId: data.instance_id}, '[MRT-B2C] B2C target info retrieved'); + + return data; +} + +/** + * Sets (creates/replaces) B2C Commerce info for a target/environment. + * + * @param options - Operation options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns Updated B2C target info + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { setB2CTargetInfo } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const info = await setB2CTargetInfo({ + * projectSlug: 'my-storefront', + * targetSlug: 'production', + * instanceId: 'aaaa_prd', + * sites: ['RefArch', 'SiteGenesis'] + * }, auth); + * + * console.log(`Connected to instance: ${info.instance_id}`); + * ``` + */ +export async function setB2CTargetInfo(options: SetB2CTargetInfoOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {projectSlug, targetSlug, instanceId, sites, origin} = options; + + logger.debug({projectSlug, targetSlug, instanceId}, '[MRT-B2C] Setting B2C target info'); + + const client = createMrtB2CClient({origin: origin || DEFAULT_MRT_B2C_ORIGIN}, auth); + + const body: B2CTargetInfo = { + instance_id: instanceId, + }; + + if (sites !== undefined) { + body.sites = sites; + } + + const {data, error} = await client.PUT('/projects/{project_slug}/b2c-target-info/{target_slug}/', { + params: { + path: {project_slug: projectSlug, target_slug: targetSlug}, + }, + body, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to set B2C target info: ${errorMessage}`); + } + + logger.debug({instanceId: data.instance_id}, '[MRT-B2C] B2C target info set'); + + return data; +} + +/** + * Updates B2C Commerce info for a target/environment. + * + * @param options - Operation options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns Updated B2C target info + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { updateB2CTargetInfo } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * // Update only the sites list + * const info = await updateB2CTargetInfo({ + * projectSlug: 'my-storefront', + * targetSlug: 'production', + * sites: ['RefArch', 'SiteGenesis', 'NewSite'] + * }, auth); + * + * console.log(`Updated sites: ${info.sites?.join(', ')}`); + * ``` + */ +export async function updateB2CTargetInfo( + options: UpdateB2CTargetInfoOptions, + auth: AuthStrategy, +): Promise { + const logger = getLogger(); + const {projectSlug, targetSlug, instanceId, sites, origin} = options; + + logger.debug({projectSlug, targetSlug, instanceId}, '[MRT-B2C] Updating B2C target info'); + + const client = createMrtB2CClient({origin: origin || DEFAULT_MRT_B2C_ORIGIN}, auth); + + const body: PatchedB2CTargetInfo = {}; + + if (instanceId !== undefined) { + body.instance_id = instanceId; + } + + if (sites !== undefined) { + body.sites = sites; + } + + const {data, error} = await client.PATCH('/projects/{project_slug}/b2c-target-info/{target_slug}/', { + params: { + path: {project_slug: projectSlug, target_slug: targetSlug}, + }, + body, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to update B2C target info: ${errorMessage}`); + } + + logger.debug({instanceId: data.instance_id}, '[MRT-B2C] B2C target info updated'); + + return data; +} diff --git a/packages/b2c-tooling-sdk/src/operations/mrt/cache.ts b/packages/b2c-tooling-sdk/src/operations/mrt/cache.ts new file mode 100644 index 00000000..1839070a --- /dev/null +++ b/packages/b2c-tooling-sdk/src/operations/mrt/cache.ts @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +/** + * Cache operations for Managed Runtime. + * + * Handles cache invalidation for MRT environments. + * + * @module operations/mrt/cache + */ +import type {AuthStrategy} from '../../auth/types.js'; +import {createMrtClient, DEFAULT_MRT_ORIGIN} from '../../clients/mrt.js'; +import {getLogger} from '../../logging/logger.js'; + +/** + * Options for invalidating cache. + */ +export interface InvalidateCacheOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * The target/environment slug. + */ + targetSlug: string; + + /** + * Path pattern to invalidate on the CDN. + * Must start with a forward slash (/). + * Use /* to invalidate all cached paths. + */ + pattern: string; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Result of cache invalidation. + */ +export interface InvalidateCacheResult { + /** + * Status message. + */ + result: string; + + /** + * Target slug. + */ + slug: string; +} + +/** + * Invalidates cached objects in the CDN for an MRT environment. + * + * Cache invalidations are asynchronous and usually complete within two minutes. + * + * @param options - Invalidation options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns Invalidation result + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { invalidateCache } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * // Invalidate all cached paths + * const result = await invalidateCache({ + * projectSlug: 'my-storefront', + * targetSlug: 'production', + * pattern: '/*' + * }, auth); + * + * console.log(result.result); + * + * // Invalidate specific path + * const result2 = await invalidateCache({ + * projectSlug: 'my-storefront', + * targetSlug: 'production', + * pattern: '/products/*' + * }, auth); + * ``` + */ +export async function invalidateCache( + options: InvalidateCacheOptions, + auth: AuthStrategy, +): Promise { + const logger = getLogger(); + const {projectSlug, targetSlug, pattern, origin} = options; + + logger.debug({projectSlug, targetSlug, pattern}, '[MRT] Invalidating cache'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.POST('/api/projects/{project_slug}/target/{target_slug}/invalidation/', { + params: { + path: {project_slug: projectSlug, target_slug: targetSlug}, + }, + body: { + pattern, + items: null, // Deprecated but required by schema + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to invalidate cache: ${errorMessage}`); + } + + // Response has different fields than request schema indicates + const response = data as unknown as {result?: string; slug?: string}; + + logger.debug({pattern}, '[MRT] Cache invalidation started'); + + return { + result: response.result ?? 'Cache invalidation is in progress.', + slug: response.slug ?? targetSlug, + }; +} diff --git a/packages/b2c-tooling-sdk/src/operations/mrt/deployment.ts b/packages/b2c-tooling-sdk/src/operations/mrt/deployment.ts new file mode 100644 index 00000000..7fa8ad22 --- /dev/null +++ b/packages/b2c-tooling-sdk/src/operations/mrt/deployment.ts @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +/** + * Deployment operations for Managed Runtime. + * + * Handles listing and creating deployments for MRT environments. + * + * @module operations/mrt/deployment + */ +import type {AuthStrategy} from '../../auth/types.js'; +import {createMrtClient, DEFAULT_MRT_ORIGIN} from '../../clients/mrt.js'; +import type {components} from '../../clients/mrt.js'; +import {getLogger} from '../../logging/logger.js'; + +/** + * Deployment list item from API. + */ +export type MrtDeployment = components['schemas']['DeployList']; + +/** + * Deployment creation request. + */ +export type MrtDeploymentCreate = components['schemas']['DeployCreate']; + +/** + * Options for listing MRT deployments. + */ +export interface ListDeploymentsOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * The target/environment slug. + */ + targetSlug: string; + + /** + * Maximum number of results to return. + */ + limit?: number; + + /** + * Offset for pagination. + */ + offset?: number; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Result of listing deployments. + */ +export interface ListDeploymentsResult { + /** + * Total count of deployments. + */ + count: number; + + /** + * URL for next page of results. + */ + next: string | null; + + /** + * URL for previous page of results. + */ + previous: string | null; + + /** + * Array of deployments. + */ + deployments: MrtDeployment[]; +} + +/** + * Lists deployment history for an MRT environment. + * + * @param options - List options including project and target slugs + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns Paginated list of deployments + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { listDeployments } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const result = await listDeployments({ + * projectSlug: 'my-storefront', + * targetSlug: 'staging' + * }, auth); + * + * for (const deploy of result.deployments) { + * console.log(`Bundle ${deploy.bundle_id}: ${deploy.status}`); + * } + * ``` + */ +export async function listDeployments( + options: ListDeploymentsOptions, + auth: AuthStrategy, +): Promise { + const logger = getLogger(); + const {projectSlug, targetSlug, limit, offset, origin} = options; + + logger.debug({projectSlug, targetSlug}, '[MRT] Listing deployments'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.GET('/api/projects/{project_slug}/target/{target_slug}/deploy/', { + params: { + path: {project_slug: projectSlug, target_slug: targetSlug}, + query: { + limit, + offset, + }, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to list deployments: ${errorMessage}`); + } + + logger.debug({count: data.count}, '[MRT] Deployments listed'); + + return { + count: data.count ?? 0, + next: data.next ?? null, + previous: data.previous ?? null, + deployments: data.results ?? [], + }; +} + +/** + * Options for creating a deployment. + */ +export interface CreateDeploymentOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * The target/environment slug. + */ + targetSlug: string; + + /** + * The bundle ID to deploy. + */ + bundleId: number; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Deployment creation response. + */ +export interface CreateDeploymentResult { + /** + * The bundle ID being deployed. + */ + bundleId: number; + + /** + * The target slug. + */ + targetSlug: string; + + /** + * Initial deployment status. + */ + status: string; +} + +/** + * Deploys a bundle to an MRT environment. + * + * This endpoint is asynchronous - the deployment will happen in the background. + * Request the target for progress updates. + * + * @param options - Deployment options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns Initial deployment status + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { createDeployment } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const result = await createDeployment({ + * projectSlug: 'my-storefront', + * targetSlug: 'staging', + * bundleId: 12345 + * }, auth); + * + * console.log(`Deployment started: ${result.status}`); + * ``` + */ +export async function createDeployment( + options: CreateDeploymentOptions, + auth: AuthStrategy, +): Promise { + const logger = getLogger(); + const {projectSlug, targetSlug, bundleId, origin} = options; + + logger.debug({projectSlug, targetSlug, bundleId}, '[MRT] Creating deployment'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {error} = await client.POST('/api/projects/{project_slug}/target/{target_slug}/deploy/', { + params: { + path: {project_slug: projectSlug, target_slug: targetSlug}, + }, + body: { + bundle_id: bundleId, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to create deployment: ${errorMessage}`); + } + + logger.debug({bundleId}, '[MRT] Deployment created'); + + return { + bundleId, + targetSlug, + status: 'pending', + }; +} diff --git a/packages/b2c-tooling-sdk/src/operations/mrt/env.ts b/packages/b2c-tooling-sdk/src/operations/mrt/env.ts index ed1b0228..49b85f26 100644 --- a/packages/b2c-tooling-sdk/src/operations/mrt/env.ts +++ b/packages/b2c-tooling-sdk/src/operations/mrt/env.ts @@ -446,3 +446,259 @@ export async function waitForEnv(options: WaitForEnvOptions, auth: AuthStrategy) throw new Error(`Timeout waiting for environment "${slug}" to be ready after ${timeout}ms`); } + +/** + * MRT environment type for updates. + */ +export type MrtEnvironmentUpdate = components['schemas']['APITargetV2Update']; + +/** + * Patched environment for partial updates. + */ +export type PatchedMrtEnvironment = components['schemas']['PatchedAPITargetV2Update']; + +/** + * Options for listing MRT environments. + */ +export interface ListEnvsOptions { + /** + * The project slug to list environments for. + */ + projectSlug: string; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Result of listing environments. + */ +export interface ListEnvsResult { + /** + * Array of environments. + */ + environments: MrtEnvironment[]; +} + +/** + * Lists environments (targets) for an MRT project. + * + * @param options - List options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns List of environments + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { listEnvs } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const result = await listEnvs({ projectSlug: 'my-storefront' }, auth); + * for (const env of result.environments) { + * console.log(`- ${env.name} (${env.slug}): ${env.state}`); + * } + * ``` + */ +export async function listEnvs(options: ListEnvsOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {projectSlug, origin} = options; + + logger.debug({projectSlug}, '[MRT] Listing environments'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.GET('/api/projects/{project_slug}/target/', { + params: { + path: {project_slug: projectSlug}, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to list environments: ${errorMessage}`); + } + + logger.debug({count: data.count}, '[MRT] Environments listed'); + + return { + environments: data.results ?? [], + }; +} + +/** + * Options for updating an MRT environment. + */ +export interface UpdateEnvOptions { + /** + * The project slug containing the environment. + */ + projectSlug: string; + + /** + * Environment slug/identifier to update. + */ + slug: string; + + /** + * New display name for the environment. + */ + name?: string; + + /** + * Mark as a production environment. + */ + isProduction?: boolean; + + /** + * Hostname pattern for V8 Tag loading. + */ + hostname?: string | null; + + /** + * Full external hostname (e.g., www.example.com). + */ + externalHostname?: string | null; + + /** + * External domain for Universal PWA SSR (e.g., example.com). + */ + externalDomain?: string | null; + + /** + * Forward HTTP cookies to origin. + */ + allowCookies?: boolean | null; + + /** + * Enable source map support in the environment. + */ + enableSourceMaps?: boolean | null; + + /** + * Minimum log level for the environment. + */ + logLevel?: LogLevel | null; + + /** + * IP whitelist (CIDR blocks, space-separated). + */ + whitelistedIps?: string | null; + + /** + * Proxy configurations for SSR. + */ + proxyConfigs?: Array<{ + path: string; + host: string; + }> | null; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Updates an environment (target) in an MRT project. + * + * Important: This endpoint automatically re-deploys the current bundle + * if any of the SSR-related properties are changed. + * + * @param options - Environment update options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns The updated environment + * @throws Error if update fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { updateEnv } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const updated = await updateEnv({ + * projectSlug: 'my-storefront', + * slug: 'staging', + * name: 'Staging v2', + * enableSourceMaps: true + * }, auth); + * ``` + */ +export async function updateEnv(options: UpdateEnvOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {projectSlug, slug, origin} = options; + + logger.debug({projectSlug, slug}, '[MRT] Updating environment'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const body: PatchedMrtEnvironment = {}; + + if (options.name !== undefined) { + body.name = options.name; + } + + if (options.isProduction !== undefined) { + body.is_production = options.isProduction; + } + + if (options.hostname !== undefined) { + body.hostname = options.hostname; + } + + if (options.externalHostname !== undefined) { + body.ssr_external_hostname = options.externalHostname; + } + + if (options.externalDomain !== undefined) { + body.ssr_external_domain = options.externalDomain; + } + + if (options.allowCookies !== undefined) { + body.allow_cookies = options.allowCookies; + } + + if (options.enableSourceMaps !== undefined) { + body.enable_source_maps = options.enableSourceMaps; + } + + if (options.logLevel !== undefined) { + body.log_level = options.logLevel; + } + + if (options.whitelistedIps !== undefined) { + body.ssr_whitelisted_ips = options.whitelistedIps; + } + + if (options.proxyConfigs !== undefined) { + body.ssr_proxy_configs = options.proxyConfigs as typeof body.ssr_proxy_configs; + } + + const {data, error} = await client.PATCH('/api/projects/{project_slug}/target/{target_slug}/', { + params: { + path: {project_slug: projectSlug, target_slug: slug}, + }, + body, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to update environment: ${errorMessage}`); + } + + logger.debug({slug: data.slug, state: data.state}, '[MRT] Environment updated'); + + return data; +} diff --git a/packages/b2c-tooling-sdk/src/operations/mrt/index.ts b/packages/b2c-tooling-sdk/src/operations/mrt/index.ts index 117e716e..f4637a29 100644 --- a/packages/b2c-tooling-sdk/src/operations/mrt/index.ts +++ b/packages/b2c-tooling-sdk/src/operations/mrt/index.ts @@ -49,9 +49,17 @@ export {createBundle, createGlobFilter, getDefaultMessage, DEFAULT_SSR_PARAMETERS} from './bundle.js'; export type {CreateBundleOptions, Bundle} from './bundle.js'; -// Push operations -export {pushBundle, uploadBundle, listBundles} from './push.js'; -export type {PushOptions, PushResult} from './push.js'; +// Push and bundle operations +export {pushBundle, uploadBundle, listBundles, downloadBundle} from './push.js'; +export type { + PushOptions, + PushResult, + ListBundlesOptions, + ListBundlesResult, + DownloadBundleOptions, + DownloadBundleResult, + MrtBundle, +} from './push.js'; // Environment variable operations export {listEnvVars, setEnvVar, setEnvVars, deleteEnvVar} from './env-var.js'; @@ -65,12 +73,152 @@ export type { } from './env-var.js'; // Environment (target) operations -export {createEnv, deleteEnv, getEnv, waitForEnv} from './env.js'; +export {createEnv, deleteEnv, getEnv, waitForEnv, listEnvs, updateEnv} from './env.js'; export type { CreateEnvOptions, DeleteEnvOptions, GetEnvOptions, WaitForEnvOptions, + ListEnvsOptions, + ListEnvsResult, + UpdateEnvOptions, MrtEnvironment, MrtEnvironmentState, + MrtEnvironmentUpdate, + PatchedMrtEnvironment, } from './env.js'; + +// Deployment operations +export {listDeployments, createDeployment} from './deployment.js'; +export type { + ListDeploymentsOptions, + ListDeploymentsResult, + CreateDeploymentOptions, + CreateDeploymentResult, + MrtDeployment, + MrtDeploymentCreate, +} from './deployment.js'; + +// Organization operations +export {listOrganizations} from './organization.js'; +export type { + ListOrganizationsOptions, + ListOrganizationsResult, + MrtOrganization, + OrganizationLimits, +} from './organization.js'; + +// Project operations +export {listProjects, createProject, getProject, updateProject, deleteProject} from './project.js'; +export type { + ListProjectsOptions, + ListProjectsResult, + CreateProjectOptions, + GetProjectOptions, + UpdateProjectOptions, + DeleteProjectOptions, + MrtProject, + MrtProjectUpdate, + PatchedMrtProject, + SsrRegion, +} from './project.js'; + +// Member operations +export {listMembers, addMember, getMember, updateMember, removeMember, MEMBER_ROLES} from './member.js'; +export type { + ListMembersOptions, + ListMembersResult, + AddMemberOptions, + GetMemberOptions, + UpdateMemberOptions, + RemoveMemberOptions, + MrtMember, + PatchedMrtMember, + MemberRoleValue, +} from './member.js'; + +// Notification operations +export { + listNotifications, + createNotification, + getNotification, + updateNotification, + deleteNotification, +} from './notification.js'; +export type { + ListNotificationsOptions, + ListNotificationsResult, + CreateNotificationOptions, + GetNotificationOptions, + UpdateNotificationOptions, + DeleteNotificationOptions, + MrtNotification, + MrtEmailNotification, + PatchedMrtNotification, +} from './notification.js'; + +// Redirect operations +export { + listRedirects, + createRedirect, + getRedirect, + updateRedirect, + deleteRedirect, + cloneRedirects, +} from './redirect.js'; +export type { + ListRedirectsOptions, + ListRedirectsResult, + CreateRedirectOptions, + GetRedirectOptions, + UpdateRedirectOptions, + DeleteRedirectOptions, + CloneRedirectsOptions, + CloneRedirectsResult, + MrtRedirect, + PatchedMrtRedirect, + RedirectHttpStatusCode, +} from './redirect.js'; + +// Access control header operations +export { + listAccessControlHeaders, + createAccessControlHeader, + getAccessControlHeader, + deleteAccessControlHeader, +} from './access-control.js'; +export type { + ListAccessControlHeadersOptions, + ListAccessControlHeadersResult, + CreateAccessControlHeaderOptions, + GetAccessControlHeaderOptions, + DeleteAccessControlHeaderOptions, + MrtAccessControlHeader, +} from './access-control.js'; + +// Cache operations +export {invalidateCache} from './cache.js'; +export type {InvalidateCacheOptions, InvalidateCacheResult} from './cache.js'; + +// User operations +export {getProfile, resetApiKey, getEmailPreferences, updateEmailPreferences} from './user.js'; +export type { + UserOperationOptions, + ApiKeyResult, + UpdateEmailPreferencesOptions, + MrtUserProfile, + MrtEmailPreferences, + PatchedMrtEmailPreferences, +} from './user.js'; + +// B2C Commerce config operations +export {getB2COrgInfo, getB2CTargetInfo, setB2CTargetInfo, updateB2CTargetInfo} from './b2c-config.js'; +export type { + GetB2COrgInfoOptions, + GetB2CTargetInfoOptions, + SetB2CTargetInfoOptions, + UpdateB2CTargetInfoOptions, + B2COrgInfo, + B2CTargetInfo, + PatchedB2CTargetInfo, +} from './b2c-config.js'; diff --git a/packages/b2c-tooling-sdk/src/operations/mrt/member.ts b/packages/b2c-tooling-sdk/src/operations/mrt/member.ts new file mode 100644 index 00000000..797ed1a6 --- /dev/null +++ b/packages/b2c-tooling-sdk/src/operations/mrt/member.ts @@ -0,0 +1,475 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +/** + * Member operations for Managed Runtime. + * + * Handles listing, adding, updating, and removing project members. + * + * @module operations/mrt/member + */ +import type {AuthStrategy} from '../../auth/types.js'; +import {createMrtClient, DEFAULT_MRT_ORIGIN} from '../../clients/mrt.js'; +import type {components} from '../../clients/mrt.js'; +import {getLogger} from '../../logging/logger.js'; + +/** + * Member type from API. + */ +export type MrtMember = components['schemas']['APIProjectMember']; + +/** + * Patched member for updates. + */ +export type PatchedMrtMember = components['schemas']['PatchedAPIProjectMember']; + +/** + * Member role values. + * 0 = Admin, 1 = Developer, 2 = Marketer, 3 = Read Only + */ +export type MemberRoleValue = 0 | 1 | 2 | 3; + +/** + * Member role names. + */ +export const MEMBER_ROLES: Record = { + 0: 'Admin', + 1: 'Developer', + 2: 'Marketer', + 3: 'Read Only', +}; + +/** + * Options for listing members. + */ +export interface ListMembersOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * Maximum number of results to return. + */ + limit?: number; + + /** + * Offset for pagination. + */ + offset?: number; + + /** + * Filter by role value. + */ + role?: MemberRoleValue; + + /** + * Search term for filtering. + */ + search?: string; + + /** + * Field to order results by. + */ + ordering?: string; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Result of listing members. + */ +export interface ListMembersResult { + /** + * Total count of members. + */ + count: number; + + /** + * URL for next page of results. + */ + next: string | null; + + /** + * URL for previous page of results. + */ + previous: string | null; + + /** + * Array of members. + */ + members: MrtMember[]; +} + +/** + * Lists members for an MRT project. + * + * @param options - List options including project slug + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns Paginated list of members + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { listMembers } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const result = await listMembers({ + * projectSlug: 'my-storefront' + * }, auth); + * + * for (const member of result.members) { + * console.log(`${member.user}: ${member.role?.name}`); + * } + * ``` + */ +export async function listMembers(options: ListMembersOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {projectSlug, limit, offset, role, search, ordering, origin} = options; + + logger.debug({projectSlug}, '[MRT] Listing members'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.GET('/api/projects/{project_slug}/members/', { + params: { + path: {project_slug: projectSlug, role: ''}, + query: { + limit, + offset, + role, + search, + ordering, + }, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to list members: ${errorMessage}`); + } + + logger.debug({count: data.count}, '[MRT] Members listed'); + + return { + count: data.count ?? 0, + next: data.next ?? null, + previous: data.previous ?? null, + members: data.results ?? [], + }; +} + +/** + * Options for adding a member. + */ +export interface AddMemberOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * Email address of the user to add. + */ + email: string; + + /** + * Role value for the member. + * 0 = Admin, 1 = Developer, 2 = Marketer, 3 = Read Only + */ + role: MemberRoleValue; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Adds a member to an MRT project. + * + * @param options - Add member options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns The created member + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { addMember } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const member = await addMember({ + * projectSlug: 'my-storefront', + * email: 'user@example.com', + * role: 1 // Developer + * }, auth); + * ``` + */ +export async function addMember(options: AddMemberOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {projectSlug, email, role, origin} = options; + + logger.debug({projectSlug, email, role}, '[MRT] Adding member'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.POST('/api/projects/{project_slug}/members/', { + params: { + path: {project_slug: projectSlug}, + }, + body: { + user: email, + role: { + value: role, + name: MEMBER_ROLES[role], + }, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to add member: ${errorMessage}`); + } + + logger.debug({email}, '[MRT] Member added'); + + return data; +} + +/** + * Options for getting a member. + */ +export interface GetMemberOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * Email address of the member. + */ + email: string; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Gets a member from an MRT project. + * + * @param options - Get member options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns The member + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { getMember } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const member = await getMember({ + * projectSlug: 'my-storefront', + * email: 'user@example.com' + * }, auth); + * + * console.log(`Role: ${member.role?.name}`); + * ``` + */ +export async function getMember(options: GetMemberOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {projectSlug, email, origin} = options; + + logger.debug({projectSlug, email}, '[MRT] Getting member'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.GET('/api/projects/{project_slug}/members/{email}/', { + params: { + path: {project_slug: projectSlug, email}, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to get member: ${errorMessage}`); + } + + logger.debug({email}, '[MRT] Member retrieved'); + + return data; +} + +/** + * Options for updating a member. + */ +export interface UpdateMemberOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * Email address of the member to update. + */ + email: string; + + /** + * New role value for the member. + * 0 = Admin, 1 = Developer, 2 = Marketer, 3 = Read Only + */ + role: MemberRoleValue; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Updates a member's role in an MRT project. + * + * @param options - Update member options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns The updated member + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { updateMember } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const member = await updateMember({ + * projectSlug: 'my-storefront', + * email: 'user@example.com', + * role: 0 // Promote to Admin + * }, auth); + * ``` + */ +export async function updateMember(options: UpdateMemberOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {projectSlug, email, role, origin} = options; + + logger.debug({projectSlug, email, role}, '[MRT] Updating member'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.PATCH('/api/projects/{project_slug}/members/{email}/', { + params: { + path: {project_slug: projectSlug, email}, + }, + body: { + role: { + value: role, + name: MEMBER_ROLES[role], + }, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to update member: ${errorMessage}`); + } + + logger.debug({email}, '[MRT] Member updated'); + + return data; +} + +/** + * Options for removing a member. + */ +export interface RemoveMemberOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * Email address of the member to remove. + */ + email: string; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Removes a member from an MRT project. + * + * @param options - Remove member options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { removeMember } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * await removeMember({ + * projectSlug: 'my-storefront', + * email: 'user@example.com' + * }, auth); + * + * console.log('Member removed'); + * ``` + */ +export async function removeMember(options: RemoveMemberOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {projectSlug, email, origin} = options; + + logger.debug({projectSlug, email}, '[MRT] Removing member'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {error} = await client.DELETE('/api/projects/{project_slug}/members/{email}/', { + params: { + path: {project_slug: projectSlug, email}, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to remove member: ${errorMessage}`); + } + + logger.debug({email}, '[MRT] Member removed'); +} diff --git a/packages/b2c-tooling-sdk/src/operations/mrt/notification.ts b/packages/b2c-tooling-sdk/src/operations/mrt/notification.ts new file mode 100644 index 00000000..7e31f663 --- /dev/null +++ b/packages/b2c-tooling-sdk/src/operations/mrt/notification.ts @@ -0,0 +1,479 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +/** + * Notification operations for Managed Runtime. + * + * Handles listing, creating, updating, and deleting project notifications. + * + * @module operations/mrt/notification + */ +import type {AuthStrategy} from '../../auth/types.js'; +import {createMrtClient, DEFAULT_MRT_ORIGIN} from '../../clients/mrt.js'; +import type {components} from '../../clients/mrt.js'; +import {getLogger} from '../../logging/logger.js'; + +/** + * Email notification type from API. + */ +export type MrtEmailNotification = components['schemas']['EmailNotification']; + +/** + * Polymorphic notification (currently only email). + */ +export type MrtNotification = components['schemas']['PolymorphicNotification']; + +/** + * Patched notification for updates. + */ +export type PatchedMrtNotification = components['schemas']['PatchedPolymorphicNotification']; + +/** + * Options for listing notifications. + */ +export interface ListNotificationsOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * Maximum number of results to return. + */ + limit?: number; + + /** + * Offset for pagination. + */ + offset?: number; + + /** + * Filter by target slug. + */ + targetSlug?: string; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Result of listing notifications. + */ +export interface ListNotificationsResult { + /** + * Total count of notifications. + */ + count: number; + + /** + * URL for next page of results. + */ + next: string | null; + + /** + * URL for previous page of results. + */ + previous: string | null; + + /** + * Array of notifications. + */ + notifications: MrtNotification[]; +} + +/** + * Lists notifications for an MRT project. + * + * @param options - List options including project slug + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns Paginated list of notifications + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { listNotifications } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const result = await listNotifications({ + * projectSlug: 'my-storefront' + * }, auth); + * + * for (const notification of result.notifications) { + * console.log(`Notification ${notification.id}`); + * } + * ``` + */ +export async function listNotifications( + options: ListNotificationsOptions, + auth: AuthStrategy, +): Promise { + const logger = getLogger(); + const {projectSlug, limit, offset, targetSlug, origin} = options; + + logger.debug({projectSlug}, '[MRT] Listing notifications'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.GET('/api/projects/{project_slug}/notifications/', { + params: { + path: {project_slug: projectSlug}, + query: { + limit, + offset, + targets__slug: targetSlug, + }, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to list notifications: ${errorMessage}`); + } + + logger.debug({count: data.count}, '[MRT] Notifications listed'); + + return { + count: data.count ?? 0, + next: data.next ?? null, + previous: data.previous ?? null, + notifications: data.results ?? [], + }; +} + +/** + * Options for creating a notification. + */ +export interface CreateNotificationOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * Target slugs to associate with this notification. + */ + targets: string[]; + + /** + * Email recipients for this notification. + */ + recipients: string[]; + + /** + * Trigger notification when deployment starts. + */ + deploymentStart?: boolean; + + /** + * Trigger notification when deployment succeeds. + */ + deploymentSuccess?: boolean; + + /** + * Trigger notification when deployment fails. + */ + deploymentFailed?: boolean; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Creates a notification for an MRT project. + * + * @param options - Create notification options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns The created notification + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { createNotification } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const notification = await createNotification({ + * projectSlug: 'my-storefront', + * targets: ['staging', 'production'], + * recipients: ['team@example.com'], + * deploymentStart: true, + * deploymentFailed: true + * }, auth); + * ``` + */ +export async function createNotification( + options: CreateNotificationOptions, + auth: AuthStrategy, +): Promise { + const logger = getLogger(); + const {projectSlug, targets, recipients, deploymentStart, deploymentSuccess, deploymentFailed, origin} = options; + + logger.debug({projectSlug, targets, recipients}, '[MRT] Creating notification'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const body: MrtNotification = { + resourcetype: 'EmailNotification', + targets, + recipients, + deployment_start: deploymentStart, + deployment_success: deploymentSuccess, + deployment_failed: deploymentFailed, + }; + + const {data, error} = await client.POST('/api/projects/{project_slug}/notifications/', { + params: { + path: {project_slug: projectSlug}, + }, + body, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to create notification: ${errorMessage}`); + } + + logger.debug({id: data.id}, '[MRT] Notification created'); + + return data; +} + +/** + * Options for getting a notification. + */ +export interface GetNotificationOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * Notification ID. + */ + notificationId: string; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Gets a notification from an MRT project. + * + * @param options - Get notification options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns The notification + * @throws Error if request fails + */ +export async function getNotification(options: GetNotificationOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {projectSlug, notificationId, origin} = options; + + logger.debug({projectSlug, notificationId}, '[MRT] Getting notification'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.GET('/api/projects/{project_slug}/notifications/{id}/', { + params: { + path: {project_slug: projectSlug, id: notificationId}, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to get notification: ${errorMessage}`); + } + + logger.debug({id: data.id}, '[MRT] Notification retrieved'); + + return data; +} + +/** + * Options for updating a notification. + */ +export interface UpdateNotificationOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * Notification ID. + */ + notificationId: string; + + /** + * Target slugs to associate with this notification. + */ + targets?: string[]; + + /** + * Email recipients for this notification. + */ + recipients?: string[]; + + /** + * Trigger notification when deployment starts. + */ + deploymentStart?: boolean; + + /** + * Trigger notification when deployment succeeds. + */ + deploymentSuccess?: boolean; + + /** + * Trigger notification when deployment fails. + */ + deploymentFailed?: boolean; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Updates a notification in an MRT project. + * + * @param options - Update notification options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns The updated notification + * @throws Error if request fails + */ +export async function updateNotification( + options: UpdateNotificationOptions, + auth: AuthStrategy, +): Promise { + const logger = getLogger(); + const { + projectSlug, + notificationId, + targets, + recipients, + deploymentStart, + deploymentSuccess, + deploymentFailed, + origin, + } = options; + + logger.debug({projectSlug, notificationId}, '[MRT] Updating notification'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const body: PatchedMrtNotification = { + resourcetype: 'EmailNotification', + }; + + if (targets !== undefined) { + body.targets = targets; + } + if (recipients !== undefined) { + body.recipients = recipients; + } + if (deploymentStart !== undefined) { + body.deployment_start = deploymentStart; + } + if (deploymentSuccess !== undefined) { + body.deployment_success = deploymentSuccess; + } + if (deploymentFailed !== undefined) { + body.deployment_failed = deploymentFailed; + } + + const {data, error} = await client.PATCH('/api/projects/{project_slug}/notifications/{id}/', { + params: { + path: {project_slug: projectSlug, id: notificationId}, + }, + body, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to update notification: ${errorMessage}`); + } + + logger.debug({id: data.id}, '[MRT] Notification updated'); + + return data; +} + +/** + * Options for deleting a notification. + */ +export interface DeleteNotificationOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * Notification ID. + */ + notificationId: string; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Deletes a notification from an MRT project. + * + * @param options - Delete notification options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @throws Error if request fails + */ +export async function deleteNotification(options: DeleteNotificationOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {projectSlug, notificationId, origin} = options; + + logger.debug({projectSlug, notificationId}, '[MRT] Deleting notification'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {error} = await client.DELETE('/api/projects/{project_slug}/notifications/{id}/', { + params: { + path: {project_slug: projectSlug, id: notificationId}, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to delete notification: ${errorMessage}`); + } + + logger.debug({notificationId}, '[MRT] Notification deleted'); +} diff --git a/packages/b2c-tooling-sdk/src/operations/mrt/organization.ts b/packages/b2c-tooling-sdk/src/operations/mrt/organization.ts new file mode 100644 index 00000000..b0c04321 --- /dev/null +++ b/packages/b2c-tooling-sdk/src/operations/mrt/organization.ts @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +/** + * Organization operations for Managed Runtime. + * + * Handles listing and retrieving MRT organizations. + * + * @module operations/mrt/organization + */ +import type {AuthStrategy} from '../../auth/types.js'; +import {createMrtClient, DEFAULT_MRT_ORIGIN} from '../../clients/mrt.js'; +import type {components} from '../../clients/mrt.js'; +import {getLogger} from '../../logging/logger.js'; + +/** + * MRT organization type from API. + */ +export type MrtOrganization = components['schemas']['APIOrganization']; + +/** + * Organization limits from API. + */ +export type OrganizationLimits = components['schemas']['OrganizationLimits']; + +/** + * Options for listing MRT organizations. + */ +export interface ListOrganizationsOptions { + /** + * Maximum number of results to return. + */ + limit?: number; + + /** + * Offset for pagination. + */ + offset?: number; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Result of listing organizations. + */ +export interface ListOrganizationsResult { + /** + * Total count of organizations. + */ + count: number; + + /** + * URL for next page of results. + */ + next: string | null; + + /** + * URL for previous page of results. + */ + previous: string | null; + + /** + * Array of organizations. + */ + organizations: MrtOrganization[]; +} + +/** + * Lists organizations accessible to the authenticated user. + * + * @param options - List options including pagination + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns Paginated list of organizations + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { listOrganizations } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const result = await listOrganizations({}, auth); + * console.log(`Found ${result.count} organizations`); + * + * for (const org of result.organizations) { + * console.log(`- ${org.name} (${org.slug})`); + * } + * ``` + */ +export async function listOrganizations( + options: ListOrganizationsOptions, + auth: AuthStrategy, +): Promise { + const logger = getLogger(); + const {limit, offset, origin} = options; + + logger.debug({limit, offset}, '[MRT] Listing organizations'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.GET('/api/organizations/', { + params: { + query: { + limit, + offset, + }, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to list organizations: ${errorMessage}`); + } + + logger.debug({count: data.count}, '[MRT] Organizations listed'); + + return { + count: data.count ?? 0, + next: data.next ?? null, + previous: data.previous ?? null, + organizations: data.results ?? [], + }; +} diff --git a/packages/b2c-tooling-sdk/src/operations/mrt/project.ts b/packages/b2c-tooling-sdk/src/operations/mrt/project.ts new file mode 100644 index 00000000..536320d4 --- /dev/null +++ b/packages/b2c-tooling-sdk/src/operations/mrt/project.ts @@ -0,0 +1,462 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +/** + * Project operations for Managed Runtime. + * + * Handles CRUD operations for MRT projects. + * + * @module operations/mrt/project + */ +import type {AuthStrategy} from '../../auth/types.js'; +import {createMrtClient, DEFAULT_MRT_ORIGIN} from '../../clients/mrt.js'; +import type {components} from '../../clients/mrt.js'; +import {getLogger} from '../../logging/logger.js'; + +/** + * MRT project type for create/read operations. + */ +export type MrtProject = components['schemas']['APIProjectV2Create']; + +/** + * MRT project type for update operations. + */ +export type MrtProjectUpdate = components['schemas']['APIProjectV2Update']; + +/** + * Patched project for partial updates. + */ +export type PatchedMrtProject = components['schemas']['PatchedAPIProjectV2Update']; + +/** + * SSR region enum. + */ +export type SsrRegion = components['schemas']['SsrRegionEnum']; + +/** + * Options for listing MRT projects. + */ +export interface ListProjectsOptions { + /** + * Filter by organization slug. + */ + organization?: string; + + /** + * Maximum number of results to return. + */ + limit?: number; + + /** + * Offset for pagination. + */ + offset?: number; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Result of listing projects. + */ +export interface ListProjectsResult { + /** + * Total count of projects. + */ + count: number; + + /** + * URL for next page of results. + */ + next: string | null; + + /** + * URL for previous page of results. + */ + previous: string | null; + + /** + * Array of projects. + */ + projects: MrtProject[]; +} + +/** + * Lists projects accessible to the authenticated user. + * + * @param options - List options including organization filter and pagination + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns Paginated list of projects + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { listProjects } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * // List all projects + * const result = await listProjects({}, auth); + * + * // List projects for a specific organization + * const orgProjects = await listProjects({ organization: 'my-org' }, auth); + * ``` + */ +export async function listProjects(options: ListProjectsOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {organization, limit, offset, origin} = options; + + logger.debug({organization, limit, offset}, '[MRT] Listing projects'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.GET('/api/projects/', { + params: { + query: { + organization, + limit, + offset, + }, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to list projects: ${errorMessage}`); + } + + logger.debug({count: data.count}, '[MRT] Projects listed'); + + return { + count: data.count ?? 0, + next: data.next ?? null, + previous: data.previous ?? null, + projects: data.results ?? [], + }; +} + +/** + * Options for creating an MRT project. + */ +export interface CreateProjectOptions { + /** + * User-friendly name for the project. + */ + name: string; + + /** + * Project slug/identifier (auto-generated if not provided). + */ + slug?: string; + + /** + * Organization slug to create the project in. + */ + organization: string; + + /** + * Project URL. + */ + url?: string; + + /** + * Default AWS region for new targets. + */ + ssrRegion?: SsrRegion; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Creates a new MRT project. + * + * @param options - Project creation options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns The created project + * @throws Error if creation fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { createProject } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const project = await createProject({ + * name: 'My Storefront', + * organization: 'my-org', + * ssrRegion: 'us-east-1' + * }, auth); + * + * console.log(`Created project: ${project.slug}`); + * ``` + */ +export async function createProject(options: CreateProjectOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {name, slug, organization, url, ssrRegion, origin} = options; + + logger.debug({name, organization}, '[MRT] Creating project'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const body: MrtProject = { + name, + organization, + }; + + if (slug) { + body.slug = slug; + } + + if (url) { + body.url = url; + } + + if (ssrRegion) { + body.ssr_region = ssrRegion; + } + + const {data, error} = await client.POST('/api/projects/', { + body, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to create project: ${errorMessage}`); + } + + logger.debug({slug: data.slug}, '[MRT] Project created'); + + return data; +} + +/** + * Options for getting an MRT project. + */ +export interface GetProjectOptions { + /** + * Project slug to retrieve. + */ + projectSlug: string; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Gets a project by slug. + * + * @param options - Get options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns The project + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { getProject } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const project = await getProject({ projectSlug: 'my-storefront' }, auth); + * console.log(`Project: ${project.name}`); + * ``` + */ +export async function getProject(options: GetProjectOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {projectSlug, origin} = options; + + logger.debug({projectSlug}, '[MRT] Getting project'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.GET('/api/projects/{project_slug}/', { + params: { + path: {project_slug: projectSlug}, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to get project: ${errorMessage}`); + } + + logger.debug({slug: data.slug}, '[MRT] Project retrieved'); + + return data; +} + +/** + * Options for updating an MRT project. + */ +export interface UpdateProjectOptions { + /** + * Project slug to update. + */ + projectSlug: string; + + /** + * New name for the project. + */ + name?: string; + + /** + * New URL for the project. + */ + url?: string; + + /** + * New default AWS region for new targets. + */ + ssrRegion?: SsrRegion; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Updates an MRT project. + * + * @param options - Update options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns The updated project + * @throws Error if update fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { updateProject } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const updated = await updateProject({ + * projectSlug: 'my-storefront', + * name: 'My Updated Storefront' + * }, auth); + * ``` + */ +export async function updateProject(options: UpdateProjectOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {projectSlug, name, url, ssrRegion, origin} = options; + + logger.debug({projectSlug}, '[MRT] Updating project'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const body: PatchedMrtProject = {}; + + if (name !== undefined) { + body.name = name; + } + + if (url !== undefined) { + body.url = url; + } + + if (ssrRegion !== undefined) { + body.ssr_region = ssrRegion; + } + + const {data, error} = await client.PATCH('/api/projects/{project_slug}/', { + params: { + path: {project_slug: projectSlug}, + }, + body, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to update project: ${errorMessage}`); + } + + logger.debug({slug: data.slug}, '[MRT] Project updated'); + + return data; +} + +/** + * Options for deleting an MRT project. + */ +export interface DeleteProjectOptions { + /** + * Project slug to delete. + */ + projectSlug: string; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Deletes an MRT project. + * + * @param options - Delete options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @throws Error if deletion fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { deleteProject } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * await deleteProject({ projectSlug: 'my-old-project' }, auth); + * console.log('Project deleted'); + * ``` + */ +export async function deleteProject(options: DeleteProjectOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {projectSlug, origin} = options; + + logger.debug({projectSlug}, '[MRT] Deleting project'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {error} = await client.DELETE('/api/projects/{project_slug}/', { + params: { + path: {project_slug: projectSlug}, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to delete project: ${errorMessage}`); + } + + logger.debug({projectSlug}, '[MRT] Project deleted'); +} diff --git a/packages/b2c-tooling-sdk/src/operations/mrt/push.ts b/packages/b2c-tooling-sdk/src/operations/mrt/push.ts index 723af077..3dc151d1 100644 --- a/packages/b2c-tooling-sdk/src/operations/mrt/push.ts +++ b/packages/b2c-tooling-sdk/src/operations/mrt/push.ts @@ -206,35 +206,206 @@ export async function uploadBundle( } /** - * Gets the list of bundles for a project. + * Bundle list item from API. + */ +export type MrtBundle = components['schemas']['BundleList']; + +/** + * Options for listing bundles. + */ +export interface ListBundlesOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * Maximum number of results to return. + */ + limit?: number; + + /** + * Offset for pagination. + */ + offset?: number; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Result of listing bundles. + */ +export interface ListBundlesResult { + /** + * Total count of bundles. + */ + count: number; + + /** + * URL for next page of results. + */ + next: string | null; + + /** + * URL for previous page of results. + */ + previous: string | null; + + /** + * Array of bundles. + */ + bundles: MrtBundle[]; +} + +/** + * Lists bundles for an MRT project. * - * @param client - MRT client instance - * @param projectSlug - Project to list bundles for - * @param options - Pagination options - * @returns List of bundles + * @param options - List options including project slug + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns Paginated list of bundles + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { listBundles } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const result = await listBundles({ + * projectSlug: 'my-storefront' + * }, auth); + * + * for (const bundle of result.bundles) { + * console.log(`Bundle ${bundle.id}: ${bundle.message}`); + * } + * ``` */ -export async function listBundles( - client: MrtClient, - projectSlug: string, - options?: {limit?: number; offset?: number}, -): Promise { +export async function listBundles(options: ListBundlesOptions, auth: AuthStrategy): Promise { const logger = getLogger(); + const {projectSlug, limit, offset, origin} = options; logger.debug({projectSlug}, '[MRT] Listing bundles'); + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + const {data, error} = await client.GET('/api/projects/{project_slug}/bundles/', { params: { path: {project_slug: projectSlug}, query: { - limit: options?.limit, - offset: options?.offset, + limit, + offset, }, }, }); if (error) { - throw new Error(`Failed to list bundles: ${JSON.stringify(error)}`); + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to list bundles: ${errorMessage}`); } - return data?.results || []; + logger.debug({count: data.count}, '[MRT] Bundles listed'); + + return { + count: data.count ?? 0, + next: data.next ?? null, + previous: data.previous ?? null, + bundles: data.results ?? [], + }; +} + +/** + * Options for downloading a bundle. + */ +export interface DownloadBundleOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * The bundle ID to download. + */ + bundleId: number; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Result of getting a bundle download URL. + */ +export interface DownloadBundleResult { + /** + * Presigned URL for downloading the bundle archive. + * Valid for one hour. + */ + downloadUrl: string; +} + +/** + * Gets a presigned URL to download a bundle archive. + * + * The returned URL is valid for one hour. + * + * @param options - Download options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns Download URL result + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { downloadBundle } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const result = await downloadBundle({ + * projectSlug: 'my-storefront', + * bundleId: 12345 + * }, auth); + * + * console.log(`Download URL: ${result.downloadUrl}`); + * ``` + */ +export async function downloadBundle( + options: DownloadBundleOptions, + auth: AuthStrategy, +): Promise { + const logger = getLogger(); + const {projectSlug, bundleId, origin} = options; + + logger.debug({projectSlug, bundleId}, '[MRT] Getting bundle download URL'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.GET('/api/projects/{project_slug}/bundles/{bundle_id}/download/', { + params: { + path: {project_slug: projectSlug, bundle_id: String(bundleId)}, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to get bundle download URL: ${errorMessage}`); + } + + logger.debug({bundleId}, '[MRT] Bundle download URL retrieved'); + + return { + downloadUrl: data.download_url, + }; } diff --git a/packages/b2c-tooling-sdk/src/operations/mrt/redirect.ts b/packages/b2c-tooling-sdk/src/operations/mrt/redirect.ts new file mode 100644 index 00000000..8fecd128 --- /dev/null +++ b/packages/b2c-tooling-sdk/src/operations/mrt/redirect.ts @@ -0,0 +1,592 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +/** + * Redirect operations for Managed Runtime. + * + * Handles listing, creating, updating, and deleting redirects for MRT environments. + * + * @module operations/mrt/redirect + */ +import type {AuthStrategy} from '../../auth/types.js'; +import {createMrtClient, DEFAULT_MRT_ORIGIN} from '../../clients/mrt.js'; +import type {components} from '../../clients/mrt.js'; +import {getLogger} from '../../logging/logger.js'; + +/** + * Redirect type from API. + */ +export type MrtRedirect = components['schemas']['APIRedirectV2CreateUpdate']; + +/** + * Patched redirect for updates. + */ +export type PatchedMrtRedirect = components['schemas']['PatchedAPIRedirectV2CreateUpdate']; + +/** + * HTTP status code for redirects (301 or 302). + */ +export type RedirectHttpStatusCode = 301 | 302; + +/** + * Options for listing redirects. + */ +export interface ListRedirectsOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * The target/environment slug. + */ + targetSlug: string; + + /** + * Maximum number of results to return. + */ + limit?: number; + + /** + * Offset for pagination. + */ + offset?: number; + + /** + * Search term for filtering. + */ + search?: string; + + /** + * Field to order results by. + */ + ordering?: string; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Result of listing redirects. + */ +export interface ListRedirectsResult { + /** + * Total count of redirects. + */ + count: number; + + /** + * URL for next page of results. + */ + next: string | null; + + /** + * URL for previous page of results. + */ + previous: string | null; + + /** + * Array of redirects. + */ + redirects: MrtRedirect[]; +} + +/** + * Lists redirects for an MRT environment. + * + * @param options - List options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns Paginated list of redirects + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { listRedirects } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const result = await listRedirects({ + * projectSlug: 'my-storefront', + * targetSlug: 'staging' + * }, auth); + * + * for (const redirect of result.redirects) { + * console.log(`${redirect.from_path} -> ${redirect.to_url}`); + * } + * ``` + */ +export async function listRedirects(options: ListRedirectsOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {projectSlug, targetSlug, limit, offset, search, ordering, origin} = options; + + logger.debug({projectSlug, targetSlug}, '[MRT] Listing redirects'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.GET('/api/projects/{project_slug}/target/{target_slug}/redirect/', { + params: { + path: {project_slug: projectSlug, target_slug: targetSlug}, + query: { + limit, + offset, + search, + ordering, + }, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to list redirects: ${errorMessage}`); + } + + logger.debug({count: data.count}, '[MRT] Redirects listed'); + + return { + count: data.count ?? 0, + next: data.next ?? null, + previous: data.previous ?? null, + redirects: data.results ?? [], + }; +} + +/** + * Options for creating a redirect. + */ +export interface CreateRedirectOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * The target/environment slug. + */ + targetSlug: string; + + /** + * The source path to redirect from. + */ + fromPath: string; + + /** + * The destination URL to redirect to. + */ + toUrl: string; + + /** + * HTTP status code (301 or 302). + * @default 301 + */ + httpStatusCode?: RedirectHttpStatusCode; + + /** + * Forward query string parameters. + */ + forwardQuerystring?: boolean; + + /** + * Forward wildcard path. + */ + forwardWildcard?: boolean; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Creates a redirect for an MRT environment. + * + * @param options - Create redirect options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns The created redirect + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { createRedirect } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const redirect = await createRedirect({ + * projectSlug: 'my-storefront', + * targetSlug: 'staging', + * fromPath: '/old-page', + * toUrl: '/new-page', + * httpStatusCode: 301 + * }, auth); + * ``` + */ +export async function createRedirect(options: CreateRedirectOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {projectSlug, targetSlug, fromPath, toUrl, httpStatusCode, forwardQuerystring, forwardWildcard, origin} = + options; + + logger.debug({projectSlug, targetSlug, fromPath, toUrl}, '[MRT] Creating redirect'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const body: MrtRedirect = { + from_path: fromPath, + to_url: toUrl, + http_status_code: httpStatusCode, + forward_querystring: forwardQuerystring, + forward_wildcard: forwardWildcard, + }; + + const {data, error} = await client.POST('/api/projects/{project_slug}/target/{target_slug}/redirect/', { + params: { + path: {project_slug: projectSlug, target_slug: targetSlug}, + }, + body, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to create redirect: ${errorMessage}`); + } + + logger.debug({fromPath}, '[MRT] Redirect created'); + + return data; +} + +/** + * Options for getting a redirect. + */ +export interface GetRedirectOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * The target/environment slug. + */ + targetSlug: string; + + /** + * The from_path of the redirect. + */ + fromPath: string; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Gets a redirect from an MRT environment. + * + * @param options - Get redirect options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns The redirect + * @throws Error if request fails + */ +export async function getRedirect(options: GetRedirectOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {projectSlug, targetSlug, fromPath, origin} = options; + + logger.debug({projectSlug, targetSlug, fromPath}, '[MRT] Getting redirect'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.GET('/api/projects/{project_slug}/target/{target_slug}/redirect/{from_path}', { + params: { + path: {project_slug: projectSlug, target_slug: targetSlug, from_path: fromPath}, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to get redirect: ${errorMessage}`); + } + + logger.debug({fromPath}, '[MRT] Redirect retrieved'); + + return data; +} + +/** + * Options for updating a redirect. + */ +export interface UpdateRedirectOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * The target/environment slug. + */ + targetSlug: string; + + /** + * The from_path of the redirect to update. + */ + fromPath: string; + + /** + * New destination URL. + */ + toUrl?: string; + + /** + * HTTP status code (301 or 302). + */ + httpStatusCode?: RedirectHttpStatusCode; + + /** + * Forward query string parameters. + */ + forwardQuerystring?: boolean; + + /** + * Forward wildcard path. + */ + forwardWildcard?: boolean; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Updates a redirect in an MRT environment. + * + * @param options - Update redirect options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns The updated redirect + * @throws Error if request fails + */ +export async function updateRedirect(options: UpdateRedirectOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {projectSlug, targetSlug, fromPath, toUrl, httpStatusCode, forwardQuerystring, forwardWildcard, origin} = + options; + + logger.debug({projectSlug, targetSlug, fromPath}, '[MRT] Updating redirect'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const body: PatchedMrtRedirect = {}; + + if (toUrl !== undefined) { + body.to_url = toUrl; + } + if (httpStatusCode !== undefined) { + body.http_status_code = httpStatusCode; + } + if (forwardQuerystring !== undefined) { + body.forward_querystring = forwardQuerystring; + } + if (forwardWildcard !== undefined) { + body.forward_wildcard = forwardWildcard; + } + + const {data, error} = await client.PATCH('/api/projects/{project_slug}/target/{target_slug}/redirect/{from_path}', { + params: { + path: {project_slug: projectSlug, target_slug: targetSlug, from_path: fromPath}, + }, + body, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to update redirect: ${errorMessage}`); + } + + logger.debug({fromPath}, '[MRT] Redirect updated'); + + return data; +} + +/** + * Options for deleting a redirect. + */ +export interface DeleteRedirectOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * The target/environment slug. + */ + targetSlug: string; + + /** + * The from_path of the redirect to delete. + */ + fromPath: string; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Deletes a redirect from an MRT environment. + * + * @param options - Delete redirect options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @throws Error if request fails + */ +export async function deleteRedirect(options: DeleteRedirectOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {projectSlug, targetSlug, fromPath, origin} = options; + + logger.debug({projectSlug, targetSlug, fromPath}, '[MRT] Deleting redirect'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {error} = await client.DELETE('/api/projects/{project_slug}/target/{target_slug}/redirect/{from_path}', { + params: { + path: {project_slug: projectSlug, target_slug: targetSlug, from_path: fromPath}, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to delete redirect: ${errorMessage}`); + } + + logger.debug({fromPath}, '[MRT] Redirect deleted'); +} + +/** + * Options for cloning redirects. + */ +export interface CloneRedirectsOptions { + /** + * The project slug. + */ + projectSlug: string; + + /** + * The source target/environment slug to clone from. + */ + fromTargetSlug: string; + + /** + * The destination target/environment slug to clone to. + */ + toTargetSlug: string; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Result of cloning redirects. + */ +export interface CloneRedirectsResult { + /** + * Number of redirects cloned. + */ + count: number; + + /** + * The cloned redirects. + */ + redirects: MrtRedirect[]; +} + +/** + * Clones redirects from one target to another. + * + * Important: When you clone redirects, you're replacing all redirects + * in the destination target with all redirects from the source target. + * + * @param options - Clone redirects options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns Result with cloned redirects + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { cloneRedirects } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const result = await cloneRedirects({ + * projectSlug: 'my-storefront', + * fromTargetSlug: 'staging', + * toTargetSlug: 'production' + * }, auth); + * + * console.log(`Cloned ${result.count} redirects`); + * ``` + */ +export async function cloneRedirects( + options: CloneRedirectsOptions, + auth: AuthStrategy, +): Promise { + const logger = getLogger(); + const {projectSlug, fromTargetSlug, toTargetSlug, origin} = options; + + logger.debug({projectSlug, fromTargetSlug, toTargetSlug}, '[MRT] Cloning redirects'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.POST('/api/projects/{project_slug}/target/{to_target_slug}/redirect/clone/', { + params: { + path: {project_slug: projectSlug, to_target_slug: toTargetSlug}, + }, + body: { + from_target_slug: fromTargetSlug, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to clone redirects: ${errorMessage}`); + } + + // The clone API may return a paginated response or just confirmation + const responseData = data as {count?: number; results?: MrtRedirect[]}; + + logger.debug({count: responseData.count}, '[MRT] Redirects cloned'); + + return { + count: responseData.count ?? 0, + redirects: responseData.results ?? [], + }; +} diff --git a/packages/b2c-tooling-sdk/src/operations/mrt/user.ts b/packages/b2c-tooling-sdk/src/operations/mrt/user.ts new file mode 100644 index 00000000..44c0930e --- /dev/null +++ b/packages/b2c-tooling-sdk/src/operations/mrt/user.ts @@ -0,0 +1,252 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +/** + * User operations for Managed Runtime. + * + * Handles user profile, API key, and email preferences management. + * + * @module operations/mrt/user + */ +import type {AuthStrategy} from '../../auth/types.js'; +import {createMrtClient, DEFAULT_MRT_ORIGIN} from '../../clients/mrt.js'; +import type {components} from '../../clients/mrt.js'; +import {getLogger} from '../../logging/logger.js'; + +/** + * User profile type from API. + */ +export type MrtUserProfile = components['schemas']['APIUserProfile']; + +/** + * User email preferences type from API. + */ +export type MrtEmailPreferences = components['schemas']['UserEmailPreferences']; + +/** + * Patched email preferences type from API. + */ +export type PatchedMrtEmailPreferences = components['schemas']['PatchedUserEmailPreferences']; + +/** + * Base options for user operations. + */ +export interface UserOperationOptions { + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Result of API key generation/reset. + */ +export interface ApiKeyResult { + /** + * The generated or reset API key. + */ + api_key: string; +} + +/** + * Options for updating email preferences. + */ +export interface UpdateEmailPreferencesOptions extends UserOperationOptions { + /** + * Whether to receive Node.js deprecation notifications. + */ + nodeDeprecationNotifications?: boolean; +} + +/** + * Gets the profile information for the authenticated user. + * + * @param options - Operation options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns User profile + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { getProfile } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const profile = await getProfile({}, auth); + * console.log(`User: ${profile.first_name} ${profile.last_name}`); + * ``` + */ +export async function getProfile(options: UserOperationOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {origin} = options; + + logger.debug('[MRT] Getting user profile'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.GET('/api/users/me/profile/'); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to get user profile: ${errorMessage}`); + } + + logger.debug({email: data.email}, '[MRT] User profile retrieved'); + + return data; +} + +/** + * Generates or resets the API key for the authenticated user. + * + * Warning: This will invalidate your current API key. + * + * @param options - Operation options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns The new API key + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { resetApiKey } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const result = await resetApiKey({}, auth); + * console.log(`New API key: ${result.api_key}`); + * // Important: Update your stored API key! + * ``` + */ +export async function resetApiKey(options: UserOperationOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {origin} = options; + + logger.debug('[MRT] Resetting API key'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.POST('/api/users/me/api_key/'); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to reset API key: ${errorMessage}`); + } + + logger.debug('[MRT] API key reset successfully'); + + // Response schema is loose in spec, cast to expected shape + const response = data as unknown as ApiKeyResult; + + return response; +} + +/** + * Gets email notification preferences for the authenticated user. + * + * @param options - Operation options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns Email preferences + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { getEmailPreferences } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const prefs = await getEmailPreferences({}, auth); + * console.log(`Node deprecation notifications: ${prefs.node_deprecation_notifications}`); + * ``` + */ +export async function getEmailPreferences( + options: UserOperationOptions, + auth: AuthStrategy, +): Promise { + const logger = getLogger(); + const {origin} = options; + + logger.debug('[MRT] Getting email preferences'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.GET('/api/users/me/email-preferences/'); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to get email preferences: ${errorMessage}`); + } + + logger.debug('[MRT] Email preferences retrieved'); + + return data; +} + +/** + * Updates email notification preferences for the authenticated user. + * + * @param options - Update options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns Updated email preferences + * @throws Error if request fails + * + * @example + * ```typescript + * import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * import { updateEmailPreferences } from '@salesforce/b2c-tooling-sdk/operations/mrt'; + * + * const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization'); + * + * const prefs = await updateEmailPreferences({ + * nodeDeprecationNotifications: false + * }, auth); + * + * console.log(`Updated: ${prefs.node_deprecation_notifications}`); + * ``` + */ +export async function updateEmailPreferences( + options: UpdateEmailPreferencesOptions, + auth: AuthStrategy, +): Promise { + const logger = getLogger(); + const {origin, nodeDeprecationNotifications} = options; + + logger.debug({nodeDeprecationNotifications}, '[MRT] Updating email preferences'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const body: PatchedMrtEmailPreferences = {}; + if (nodeDeprecationNotifications !== undefined) { + body.node_deprecation_notifications = nodeDeprecationNotifications; + } + + const {data, error} = await client.PATCH('/api/users/me/email-preferences/', { + body, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to update email preferences: ${errorMessage}`); + } + + logger.debug('[MRT] Email preferences updated'); + + return data; +} diff --git a/packages/b2c-tooling-sdk/test/operations/mrt/push.test.ts b/packages/b2c-tooling-sdk/test/operations/mrt/push.test.ts index c5b47173..f42ee703 100644 --- a/packages/b2c-tooling-sdk/test/operations/mrt/push.test.ts +++ b/packages/b2c-tooling-sdk/test/operations/mrt/push.test.ts @@ -174,13 +174,13 @@ describe('operations/mrt/push', () => { ); const auth = new MockAuthStrategy(); - const client = createMrtClient({}, auth); - const bundles = await listBundles(client, 'my-project'); + const result = await listBundles({projectSlug: 'my-project'}, auth); - expect(bundles).to.have.length(2); - expect(bundles[0]).to.deep.include({id: 123, message: 'Bundle 1'}); - expect(bundles[1]).to.deep.include({id: 124, message: 'Bundle 2'}); + expect(result.bundles).to.have.length(2); + expect(result.bundles[0]).to.deep.include({id: 123, message: 'Bundle 1'}); + expect(result.bundles[1]).to.deep.include({id: 124, message: 'Bundle 2'}); + expect(result.count).to.equal(2); }); it('passes pagination options', async () => { @@ -189,14 +189,13 @@ describe('operations/mrt/push', () => { server.use( http.get(`${DEFAULT_BASE_URL}/api/projects/:projectSlug/bundles/`, ({request}) => { queryParams = new URL(request.url).searchParams; - return HttpResponse.json({results: []}); + return HttpResponse.json({count: 0, results: []}); }), ); const auth = new MockAuthStrategy(); - const client = createMrtClient({}, auth); - await listBundles(client, 'my-project', {limit: 10, offset: 20}); + await listBundles({projectSlug: 'my-project', limit: 10, offset: 20}, auth); expect(queryParams?.get('limit')).to.equal('10'); expect(queryParams?.get('offset')).to.equal('20'); @@ -213,11 +212,11 @@ describe('operations/mrt/push', () => { ); const auth = new MockAuthStrategy(); - const client = createMrtClient({}, auth); - const bundles = await listBundles(client, 'my-project'); + const result = await listBundles({projectSlug: 'my-project'}, auth); - expect(bundles).to.have.length(0); + expect(result.bundles).to.have.length(0); + expect(result.count).to.equal(0); }); it('throws error on API failure', async () => { @@ -228,10 +227,9 @@ describe('operations/mrt/push', () => { ); const auth = new MockAuthStrategy(); - const client = createMrtClient({}, auth); try { - await listBundles(client, 'my-project'); + await listBundles({projectSlug: 'my-project'}, auth); expect.fail('Should have thrown'); } catch (error) { expect((error as Error).message).to.include('Failed to list bundles'); diff --git a/plugins/b2c-cli/skills/b2c-mrt/SKILL.md b/plugins/b2c-cli/skills/b2c-mrt/SKILL.md index 7271e2ea..71abc8a8 100644 --- a/plugins/b2c-cli/skills/b2c-mrt/SKILL.md +++ b/plugins/b2c-cli/skills/b2c-mrt/SKILL.md @@ -5,63 +5,148 @@ description: Using the b2c CLI for Managed Runtime (MRT) project and deployment # B2C MRT Skill -Use the `b2c` CLI plugin to manage Managed Runtime (MRT) projects and deployments for PWA Kit storefronts. +Use the `b2c` CLI to manage Managed Runtime (MRT) projects, environments, bundles, and deployments for PWA Kit storefronts. -## Examples +## Command Structure -### Push Bundle to Managed Runtime +``` +mrt +├── org (list, b2c) - Organizations and B2C connections +├── project - Project management +│ ├── member - Team member management +│ └── notification - Deployment notifications +├── env - Environment management +│ ├── var - Environment variables +│ ├── redirect - URL redirects +│ └── access-control - Access control headers +├── bundle - Bundle and deployment management +└── user - User profile and settings +``` + +## Quick Examples + +### Deploy a Bundle ```bash -# push a bundle to MRT for a specific project -b2c mrt push --project my-storefront +# Push local build to staging +b2c mrt bundle deploy -p my-storefront -e staging + +# Push to production with release message +b2c mrt bundle deploy -p my-storefront -e production -m "Release v1.0.0" -# push to a specific environment (staging, production, etc.) -b2c mrt push --project my-storefront --environment staging +# Deploy existing bundle by ID +b2c mrt bundle deploy 12345 -p my-storefront -e production +``` -# push to production with a release message -b2c mrt push --project my-storefront --environment production --message "Release v1.0.0" +### Manage Environments -# push from a custom build directory -b2c mrt push --project my-storefront --build-dir ./dist +```bash +# List environments +b2c mrt env list -p my-storefront -# specify Node.js version for SSR runtime -b2c mrt push --project my-storefront --node-version 20.x +# Create a new environment +b2c mrt env create qa -p my-storefront --name "QA Environment" -# add SSR parameters -b2c mrt push --project my-storefront --ssr-param SSRProxyPath=/api +# Get environment details +b2c mrt env get -p my-storefront -e production -# use JSON output for automation -b2c mrt push --project my-storefront --json +# Invalidate CDN cache +b2c mrt env invalidate -p my-storefront -e production ``` -### Manage Environments +### Environment Variables + +```bash +# List variables +b2c mrt env var list -p my-storefront -e production + +# Set variables +b2c mrt env var set API_KEY=secret DEBUG=true -p my-storefront -e staging + +# Delete a variable +b2c mrt env var delete OLD_VAR -p my-storefront -e production +``` + +### View Deployment History + +```bash +# List bundles in project +b2c mrt bundle list -p my-storefront + +# View deployment history for environment +b2c mrt bundle history -p my-storefront -e production + +# Download a bundle artifact +b2c mrt bundle download 12345 -p my-storefront +``` + +### Project Management ```bash -# create a new MRT environment -b2c mrt env create +# List projects +b2c mrt project list + +# Get project details +b2c mrt project get -p my-storefront + +# List project members +b2c mrt project member list -p my-storefront -# delete an MRT environment -b2c mrt env delete +# Add a member +b2c mrt project member add user@example.com -p my-storefront --role developer +``` + +### URL Redirects + +```bash +# List redirects +b2c mrt env redirect list -p my-storefront -e production + +# Create a redirect +b2c mrt env redirect create -p my-storefront -e production \ + --from "/old-path" --to "/new-path" + +# Clone redirects between environments +b2c mrt env redirect clone -p my-storefront --source staging --target production +``` + +## Configuration + +### dw.json + +Configure MRT settings in your project's `dw.json`: + +```json +{ + "mrtProject": "my-storefront", + "mrtEnvironment": "staging" +} ``` ### Environment Variables ```bash -# manage environment variables for an MRT environment -b2c mrt env var +export SFCC_MRT_API_KEY=your-api-key +export SFCC_MRT_PROJECT=my-storefront +export SFCC_MRT_ENVIRONMENT=staging ``` -### Configuration +### ~/.mobify Config + +Store your API key in `~/.mobify`: + +```json +{ + "api_key": "your-mrt-api-key" +} +``` -MRT settings can be configured in `dw.json`: -- `mrtProject`: MRT project slug -- `mrtEnvironment`: MRT environment name (staging, production, etc.) +## Detailed References -Environment variables: -- `SFCC_MRT_PROJECT`: MRT project slug -- `SFCC_MRT_ENVIRONMENT`: MRT environment -- `SFCC_MRT_API_KEY`: MRT API key +- [Project Commands](references/PROJECT-COMMANDS.md) - Projects, members, and notifications +- [Environment Commands](references/ENVIRONMENT-COMMANDS.md) - Environments, variables, redirects +- [Bundle Commands](references/BUNDLE-COMMANDS.md) - Deployments, history, downloads ### More Commands -See `b2c mrt --help` for a full list of available commands and options in the `mrt` topic. +See `b2c mrt --help` for a full list of available commands and options. diff --git a/plugins/b2c-cli/skills/b2c-mrt/references/BUNDLE-COMMANDS.md b/plugins/b2c-cli/skills/b2c-mrt/references/BUNDLE-COMMANDS.md new file mode 100644 index 00000000..f35a27d3 --- /dev/null +++ b/plugins/b2c-cli/skills/b2c-mrt/references/BUNDLE-COMMANDS.md @@ -0,0 +1,137 @@ +# MRT Bundle Commands Reference + +Detailed reference for MRT bundle deployment, listing, history, and download commands. + +## Bundle Deploy + +Push a local build or deploy an existing bundle to Managed Runtime. + +### Push Local Build + +```bash +# Push local build to project (no deployment) +b2c mrt bundle deploy --project my-storefront + +# Push and deploy to staging +b2c mrt bundle deploy -p my-storefront -e staging + +# Push and deploy to production with message +b2c mrt bundle deploy -p my-storefront -e production --message "Release v1.0.0" + +# Push from custom build directory +b2c mrt bundle deploy -p my-storefront --build-dir ./dist + +# Specify Node.js version +b2c mrt bundle deploy -p my-storefront --node-version 20.x + +# Set SSR parameters +b2c mrt bundle deploy -p my-storefront --ssr-param SSRProxyPath=/api + +# Multiple SSR parameters +b2c mrt bundle deploy -p my-storefront \ + --ssr-param SSRProxyPath=/api \ + --ssr-param SSRTimeout=30000 +``` + +### Deploy Existing Bundle + +```bash +# Deploy existing bundle by ID +b2c mrt bundle deploy 12345 -p my-storefront -e production + +# Deploy with JSON output +b2c mrt bundle deploy 12345 -p my-storefront -e staging --json +``` + +**Flags:** +| Flag | Description | Default | +|------|-------------|---------| +| `--message`, `-m` | Bundle message/description | | +| `--build-dir`, `-b` | Path to build directory | `build` | +| `--ssr-only` | Server-only file patterns | `ssr.js,ssr.mjs,server/**/*` | +| `--ssr-shared` | Shared file patterns | `static/**/*,client/**/*` | +| `--node-version`, `-n` | Node.js version for SSR | `22.x` | +| `--ssr-param` | SSR parameters (key=value, can repeat) | | + +## Bundle List + +List bundles in a project. + +```bash +b2c mrt bundle list --project my-storefront +b2c mrt bundle list -p my-storefront --limit 10 +b2c mrt bundle list -p my-storefront --offset 20 +b2c mrt bundle list -p my-storefront --json +``` + +**Output columns:** Bundle ID, Message, Status, Created + +## Bundle History + +View deployment history for an environment. + +```bash +b2c mrt bundle history -p my-storefront -e production +b2c mrt bundle history -p my-storefront -e staging --limit 5 +b2c mrt bundle history -p my-storefront -e production --json +``` + +**Output columns:** Bundle ID, Message, Status, Type, Created + +## Bundle Download + +Download a bundle artifact. + +```bash +# Download to current directory (bundle-{id}.tgz) +b2c mrt bundle download 12345 -p my-storefront + +# Download to specific path +b2c mrt bundle download 12345 -p my-storefront -o ./artifacts/bundle.tgz + +# Get download URL only (for use in scripts) +b2c mrt bundle download 12345 -p my-storefront --url-only + +# JSON output with download URL +b2c mrt bundle download 12345 -p my-storefront --json +``` + +## Common Workflows + +### Development to Production Pipeline + +```bash +# 1. Build your PWA Kit application +npm run build + +# 2. Push to staging for testing +b2c mrt bundle deploy -p my-storefront -e staging -m "v1.0.0-rc1" + +# 3. After testing, deploy same bundle to production +# First, find the bundle ID from the staging deployment +b2c mrt bundle list -p my-storefront --limit 1 + +# 4. Deploy that bundle to production +b2c mrt bundle deploy 12345 -p my-storefront -e production +``` + +### Rollback to Previous Bundle + +```bash +# 1. View deployment history +b2c mrt bundle history -p my-storefront -e production + +# 2. Deploy previous bundle +b2c mrt bundle deploy 12340 -p my-storefront -e production +``` + +### Download and Inspect Bundle + +```bash +# Download the bundle +b2c mrt bundle download 12345 -p my-storefront -o bundle.tgz + +# Extract and inspect +tar -xzf bundle.tgz +ls -la +``` diff --git a/plugins/b2c-cli/skills/b2c-mrt/references/ENVIRONMENT-COMMANDS.md b/plugins/b2c-cli/skills/b2c-mrt/references/ENVIRONMENT-COMMANDS.md new file mode 100644 index 00000000..5afc048d --- /dev/null +++ b/plugins/b2c-cli/skills/b2c-mrt/references/ENVIRONMENT-COMMANDS.md @@ -0,0 +1,185 @@ +# MRT Environment Commands Reference + +Detailed reference for MRT environment, variable, redirect, and access control commands. + +## Environment Management + +### List Environments + +```bash +b2c mrt env list --project my-storefront +b2c mrt env list -p my-storefront --json +``` + +### Create Environment + +```bash +# Basic staging environment +b2c mrt env create staging --project my-storefront --name "Staging Environment" + +# Production environment in specific region +b2c mrt env create production -p my-storefront --name "Production" \ + --production --region eu-west-1 + +# With external hostname configuration +b2c mrt env create prod -p my-storefront --name "Production" \ + --production \ + --external-hostname www.example.com \ + --external-domain example.com + +# With cookie forwarding and source maps +b2c mrt env create dev -p my-storefront --name "Development" \ + --allow-cookies --enable-source-maps +``` + +**Flags:** +| Flag | Description | +|------|-------------| +| `--name`, `-n` | Display name (required) | +| `--region`, `-r` | AWS region for SSR deployment | +| `--production` | Mark as production environment | +| `--hostname` | Hostname pattern for V8 Tag loading | +| `--external-hostname` | Full external hostname (e.g., www.example.com) | +| `--external-domain` | External domain for Universal PWA SSR | +| `--allow-cookies` | Forward HTTP cookies to origin | +| `--enable-source-maps` | Enable source map support | + +### Get Environment Details + +```bash +b2c mrt env get --project my-storefront --environment staging +b2c mrt env get -p my-storefront -e production --json +``` + +### Update Environment + +```bash +b2c mrt env update -p my-storefront -e staging --name "Updated Staging" +b2c mrt env update -p my-storefront -e production --allow-cookies +b2c mrt env update -p my-storefront -e dev --no-enable-source-maps +``` + +### Delete Environment + +```bash +b2c mrt env delete staging --project my-storefront +b2c mrt env delete old-env -p my-storefront --force +``` + +### Invalidate Cache + +Invalidate CDN cached content for an environment. + +```bash +# Invalidate all cached content +b2c mrt env invalidate -p my-storefront -e production + +# Invalidate specific paths +b2c mrt env invalidate -p my-storefront -e production \ + --path "/products/*" --path "/categories/*" +``` + +### B2C Commerce Connection + +Get or set B2C Commerce instance connection for an environment. + +```bash +# Get current configuration +b2c mrt env b2c -p my-storefront -e production + +# Set B2C instance +b2c mrt env b2c -p my-storefront -e production --instance-id aaaa_prd + +# Set B2C instance with specific sites +b2c mrt env b2c -p my-storefront -e production \ + --instance-id aaaa_prd --sites RefArch,SiteGenesis + +# Clear sites list +b2c mrt env b2c -p my-storefront -e production --clear-sites +``` + +## Environment Variables + +### List Variables + +```bash +b2c mrt env var list --project my-storefront --environment production +b2c mrt env var list -p my-storefront -e staging --json +``` + +### Set Variables + +```bash +# Single variable +b2c mrt env var set MY_VAR=value -p my-storefront -e production + +# Multiple variables +b2c mrt env var set API_KEY=secret DEBUG=true FEATURE_FLAG=enabled \ + -p my-storefront -e staging + +# Value with spaces (use quotes) +b2c mrt env var set "MESSAGE=hello world" -p my-storefront -e production + +# Using environment variables for auth +export SFCC_MRT_API_KEY=your-api-key +export SFCC_MRT_PROJECT=my-storefront +export SFCC_MRT_ENVIRONMENT=staging +b2c mrt env var set MY_VAR=value +``` + +### Delete Variable + +```bash +b2c mrt env var delete MY_VAR -p my-storefront -e production +``` + +## URL Redirects + +### List Redirects + +```bash +b2c mrt env redirect list -p my-storefront -e production +b2c mrt env redirect list -p my-storefront -e production --limit 50 +b2c mrt env redirect list -p my-storefront -e production --json +``` + +### Create Redirect + +```bash +# Basic redirect +b2c mrt env redirect create -p my-storefront -e production \ + --from "/old-path" --to "/new-path" + +# Permanent redirect (301) +b2c mrt env redirect create -p my-storefront -e production \ + --from "/legacy/*" --to "/modern/$1" --permanent + +# Temporary redirect (302, default) +b2c mrt env redirect create -p my-storefront -e production \ + --from "/promo" --to "/sale" +``` + +### Delete Redirect + +```bash +b2c mrt env redirect delete abc-123 -p my-storefront -e production +b2c mrt env redirect delete abc-123 -p my-storefront -e production --force +``` + +### Clone Redirects + +Copy redirects from one environment to another. + +```bash +b2c mrt env redirect clone -p my-storefront \ + --source staging --target production +``` + +## Access Control Headers + +### List Access Control Headers + +```bash +b2c mrt env access-control list -p my-storefront -e staging +b2c mrt env access-control list -p my-storefront -e production --json +``` diff --git a/plugins/b2c-cli/skills/b2c-mrt/references/PROJECT-COMMANDS.md b/plugins/b2c-cli/skills/b2c-mrt/references/PROJECT-COMMANDS.md new file mode 100644 index 00000000..edc31a63 --- /dev/null +++ b/plugins/b2c-cli/skills/b2c-mrt/references/PROJECT-COMMANDS.md @@ -0,0 +1,144 @@ +# MRT Project Commands Reference + +Detailed reference for MRT project, member, and notification commands. + +## Project Management + +### List Projects + +```bash +b2c mrt project list +b2c mrt project list --limit 10 --offset 0 +b2c mrt project list --json +``` + +### Create Project + +```bash +b2c mrt project create my-storefront --name "My Storefront" +b2c mrt project create my-storefront --name "My Storefront" --organization my-org +``` + +### Get Project Details + +```bash +b2c mrt project get --project my-storefront +b2c mrt project get -p my-storefront --json +``` + +### Update Project + +```bash +b2c mrt project update --project my-storefront --name "Updated Name" +``` + +### Delete Project + +```bash +b2c mrt project delete --project my-storefront +b2c mrt project delete -p my-storefront --force # skip confirmation +``` + +## Member Management + +Members can have one of three roles: `admin`, `developer`, or `viewer`. + +### List Members + +```bash +b2c mrt project member list --project my-storefront +b2c mrt project member list -p my-storefront --json +``` + +### Add Member + +```bash +b2c mrt project member add user@example.com --project my-storefront --role admin +b2c mrt project member add user@example.com -p my-storefront --role developer +b2c mrt project member add user@example.com -p my-storefront --role viewer +``` + +### Get Member Details + +```bash +b2c mrt project member get user@example.com --project my-storefront +``` + +### Update Member Role + +```bash +b2c mrt project member update user@example.com --project my-storefront --role viewer +``` + +### Remove Member + +```bash +b2c mrt project member remove user@example.com --project my-storefront +b2c mrt project member remove user@example.com -p my-storefront --force +``` + +## Deployment Notifications + +Configure email notifications for deployment events (start, success, failure). + +### List Notifications + +```bash +b2c mrt project notification list --project my-storefront +b2c mrt project notification list -p my-storefront --json +``` + +### Create Notification + +```bash +# Notify on deployment failures only +b2c mrt project notification create -p my-storefront \ + --target staging --target production \ + --recipient ops@example.com \ + --on-failed + +# Notify on all deployment events +b2c mrt project notification create -p my-storefront \ + --target production \ + --recipient team@example.com \ + --on-start --on-success --on-failed + +# Multiple recipients +b2c mrt project notification create -p my-storefront \ + --target production \ + --recipient dev@example.com --recipient ops@example.com \ + --on-failed +``` + +**Flags:** +| Flag | Description | +|------|-------------| +| `--target`, `-t` | Target environment (can specify multiple) | +| `--recipient`, `-r` | Email recipient (can specify multiple) | +| `--on-start` | Notify when deployment starts | +| `--on-success` | Notify when deployment succeeds | +| `--on-failed` | Notify when deployment fails | + +### Get Notification Details + +```bash +b2c mrt project notification get abc-123 --project my-storefront +b2c mrt project notification get abc-123 -p my-storefront --json +``` + +### Update Notification + +```bash +# Change notification events +b2c mrt project notification update abc-123 -p my-storefront --on-start --no-on-failed + +# Update recipients +b2c mrt project notification update abc-123 -p my-storefront --recipient new-team@example.com +``` + +### Delete Notification + +```bash +b2c mrt project notification delete abc-123 --project my-storefront +b2c mrt project notification delete abc-123 -p my-storefront --force +```