diff --git a/.changeset/mrt-utilities-dev-data-store.md b/.changeset/mrt-utilities-dev-data-store.md new file mode 100644 index 00000000..70481acf --- /dev/null +++ b/.changeset/mrt-utilities-dev-data-store.md @@ -0,0 +1,5 @@ +--- +'@salesforce/mrt-utilities': patch +--- + +Add a development-mode pseudo data-store implementation for `@salesforce/mrt-utilities/data-store` with environment-variable-backed defaults, while preserving the existing public API and production behavior. diff --git a/docs/guide/mrt-utilities.md b/docs/guide/mrt-utilities.md index 1e44a646..9d4540b3 100644 --- a/docs/guide/mrt-utilities.md +++ b/docs/guide/mrt-utilities.md @@ -149,6 +149,37 @@ import { MetricsSender } from '@salesforce/mrt-utilities/metrics'; Use when you need to emit metrics from the same process that serves requests (e.g. custom middleware or request processor). +## Data Store In Development + +The production MRT data store is not available during local development because it depends on deployed runtime infrastructure. `@salesforce/mrt-utilities` provides an equivalent development data-store implementation for local use. + +To use that local equivalent, import from the `data-store` subpath and run Node with the `dev-data-store` condition: + +```bash +node --conditions dev-data-store server.js +``` + +```typescript +import {DataStore} from '@salesforce/mrt-utilities/data-store'; + +const store = DataStore.getDataStore(); +const entry = await store.getEntry('custom-global-preferences'); +``` + +Provide local data-store values through environment variables: + +- `SFNEXT_DATA_STORE_DEFAULTS`: JSON map of data-store keys to object values +- `SFNEXT_DATA_STORE_WARN_ON_MISSING`: set to `false` to suppress missing-key warnings + +Example: + +```bash +export SFNEXT_DATA_STORE_DEFAULTS='{"custom-global-preferences":{"featureFlag":true}}' +export SFNEXT_DATA_STORE_WARN_ON_MISSING=true +``` + +The development pseudo store keeps production parity for missing keys and throws `DataStoreNotFoundError` when a key is not found. + ## Related - [MRT CLI commands](/cli/mrt) — manage MRT projects, environments, and bundles from the CLI. diff --git a/packages/mrt-utilities/DEV-DATA-STORE-PLAN.md b/packages/mrt-utilities/DEV-DATA-STORE-PLAN.md new file mode 100644 index 00000000..6ab4107b --- /dev/null +++ b/packages/mrt-utilities/DEV-DATA-STORE-PLAN.md @@ -0,0 +1,135 @@ +# Development Data Store Plan (`@salesforce/mrt-utilities`) + +## Goal + +Add a pseudo local data-store implementation for development mode so local runtimes do not fail when DynamoDB-backed MRT data store is unavailable. + +This behavior should be activated through the existing package export condition: + +- `@salesforce/mrt-utilities/data-store` + `--conditions development` + +## Key Requirement + +The pseudo local implementation must read default entry values from environment variables (as implied by the provided prototype): + +- `SFNEXT_DATA_STORE_DEFAULTS` (JSON object map of key -> value object) +- `SFNEXT_DATA_STORE_WARN_ON_MISSING` (`"false"` disables warnings; default is warning enabled) + +## Current State + +- `./data-store` already has a `development` export condition in `package.json`, but it currently points to `src`. +- Desired update: point `development` to a built `dist` pseudo-local data-store output. +- Current `DataStore` implementation is DynamoDB-based and throws `DataStoreUnavailableError` when required MRT environment variables are missing. +- Existing tests primarily validate production/DynamoDB behavior. + +## Proposed Design + +## 1) Split data-store implementations + +Create two implementation modules: + +- **Production implementation** (existing behavior) + - DynamoDB-backed + - Preserves existing errors: + - `DataStoreUnavailableError` + - `DataStoreNotFoundError` + - `DataStoreServiceError` +- **Development implementation** (new behavior) + - No AWS dependency + - Uses local defaults from env var JSON + - Warns once per missing key (configurable) + - Uses strict parity with production semantics by default for missing keys (throws not-found) + - Optional lenient mode can be introduced as explicit opt-in for `{}` fallback during local experimentation + +## 2) Preserve stable public API + +Keep consumer import surface unchanged: + +- `DataStore.getDataStore()` +- `DataStore#isDataStoreAvailable()` +- `DataStore#getEntry(key)` +- Existing error classes remain exported + +This allows existing projects to adopt dev behavior without refactoring imports. + +## 3) Route development exports + +Use conditional exports to load the development implementation for local dev from built artifacts: + +- `development` -> built dev pseudo-local data-store module in `dist` +- `import` / `require` -> production built outputs in `dist` + +## 4) Environment variable behavior in dev store + +### `SFNEXT_DATA_STORE_DEFAULTS` + +- Parse as JSON object. +- Expected shape: + - `{ "": { ...objectValue } }` +- On invalid JSON: + - fall back to empty defaults + - warn once with clear message + +### `SFNEXT_DATA_STORE_WARN_ON_MISSING` + +- If unset: warnings enabled +- If set to `"false"` (case-insensitive): disable missing-key warnings +- Any other value: warnings enabled + +### Missing key semantics (dev mode) + +- If key exists in parsed defaults and value is object: return that value. +- If key missing or invalid value type: + - by default, throw `DataStoreNotFoundError` (production parity) + - optionally warn once for that key before throwing + - optional future opt-in lenient mode may return `{}` instead (must be off by default) + +## 5) Tests + +Add/adjust tests to cover both modes: + +- **Production tests** + - Keep current behavior assertions unchanged. +- **Development tests** + - Reads defaults from `SFNEXT_DATA_STORE_DEFAULTS` + - Throws `DataStoreNotFoundError` when key is absent (default behavior) + - Warns once per missing key when warnings enabled + - Does not warn when `SFNEXT_DATA_STORE_WARN_ON_MISSING=false` + - Handles invalid JSON safely + - (If lenient mode is added) returns `{}` only when explicitly enabled + +## 6) Documentation updates + +Update `packages/mrt-utilities/README.md` (or docs page if preferred) with: + +- How to enable dev behavior (`node --conditions development`) +- Env var configuration examples for default data-store values +- Differences between dev and production data-store semantics + +## Implementation Steps + +1. Add a new dev data-store module in `src/data-store/`. +2. Move/keep current DynamoDB implementation as production module. +3. Ensure build output emits both implementations to `dist` (esm/cjs + types). +4. Update `package.json` exports so `development` resolves to the built dev pseudo-local module in `dist` (not `src`), while `import`/`require` continue resolving to production built outputs. +5. Add development-focused tests. +6. Run validation: + - `pnpm --filter @salesforce/mrt-utilities run test:agent` + - `pnpm --filter @salesforce/mrt-utilities run lint:agent` + - `pnpm --filter @salesforce/mrt-utilities run typecheck:agent` +7. Add a changeset for `@salesforce/mrt-utilities` if this is considered user-facing behavior. + +## Risks / Notes + +- Strict production parity in dev is the default to avoid masking missing-key issues. +- Any lenient `{}` fallback behavior must be explicit opt-in and clearly documented. +- Existing export stripping behavior is already understood and is not changed by this plan. + +## Acceptance Criteria + +- Local development using `--conditions development` no longer fails due to missing DynamoDB/MRT runtime vars. +- Dev data-store entries are sourced from `SFNEXT_DATA_STORE_DEFAULTS`. +- Missing-key behavior is predictable and configurable via `SFNEXT_DATA_STORE_WARN_ON_MISSING`. +- Production behavior and API remain backward-compatible. +- No breaking public interface changes: existing import paths, exported symbols, and type surface for `@salesforce/mrt-utilities` and `@salesforce/mrt-utilities/data-store` remain intact (except correcting the `development` export target to built `dist` output). +- Default dev missing-key semantics match production (`DataStoreNotFoundError`), with no implicit `{}` fallback. diff --git a/packages/mrt-utilities/README.md b/packages/mrt-utilities/README.md index 6b880770..d03356a6 100644 --- a/packages/mrt-utilities/README.md +++ b/packages/mrt-utilities/README.md @@ -47,3 +47,32 @@ export const createApp = (): Express => { // Cleans up any remaining headers and sets any remaining values app.use(createMRTCleanUpMiddleware()); ``` + +## Development data-store usage + +Use the `data-store` subpath with Node's `dev-data-store` condition to load the pseudo local data-store implementation: + +```bash +node --conditions dev-data-store your-app.js +``` + +```ts +import {DataStore} from '@salesforce/mrt-utilities/data-store'; + +const store = DataStore.getDataStore(); +const entry = await store.getEntry('custom-global-preferences'); +``` + +Configure local values with environment variables: + +- `SFNEXT_DATA_STORE_DEFAULTS`: JSON map of key to object value +- `SFNEXT_DATA_STORE_WARN_ON_MISSING`: set to `false` to suppress missing-key warnings + +Example: + +```bash +export SFNEXT_DATA_STORE_DEFAULTS='{"custom-global-preferences":{"featureFlag":true}}' +export SFNEXT_DATA_STORE_WARN_ON_MISSING=true +``` + +By default, missing keys still throw `DataStoreNotFoundError` in development (matching production semantics). diff --git a/packages/mrt-utilities/package.json b/packages/mrt-utilities/package.json index 8379910f..d0eab6e2 100644 --- a/packages/mrt-utilities/package.json +++ b/packages/mrt-utilities/package.json @@ -52,6 +52,16 @@ } }, "./data-store": { + "dev-data-store": { + "import": { + "types": "./dist/esm/data-store/development.d.ts", + "default": "./dist/esm/data-store/development.js" + }, + "require": { + "types": "./dist/cjs/data-store/development.d.ts", + "default": "./dist/cjs/data-store/development.js" + } + }, "development": "./src/data-store/index.ts", "import": { "types": "./dist/esm/data-store/index.d.ts", diff --git a/packages/mrt-utilities/src/data-store/development.ts b/packages/mrt-utilities/src/data-store/development.ts new file mode 100644 index 00000000..0d8032e3 --- /dev/null +++ b/packages/mrt-utilities/src/data-store/development.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 {DataStoreNotFoundError} from './errors.js'; + +export {DataStoreNotFoundError, DataStoreServiceError, DataStoreUnavailableError} from './errors.js'; + +/** + * Development-only pseudo data store backed by environment variables. + * + * This class mirrors the public DataStore API while avoiding DynamoDB access. + */ +export class DataStore { + private defaults: Record>; + + private warnOnMissing: boolean; + + private warnedKeys: Set; + + private static _instance: DataStore | null = null; + + private constructor() { + this.defaults = readDefaultsFromEnv(); + this.warnOnMissing = readWarnOnMissingFromEnv(); + this.warnedKeys = new Set(); + } + + /** + * Get or create the singleton DataStore instance. + * + * @returns The singleton DataStore instance + */ + static getDataStore(): DataStore { + if (!DataStore._instance) { + DataStore._instance = new DataStore(); + } + + return DataStore._instance; + } + + /** + * Whether the data store can be used in the current environment. + * + * The development pseudo store is always available when loaded. + * + * @returns true + */ + isDataStoreAvailable(): boolean { + return true; + } + + /** + * Fetch an entry from the pseudo data store. + * + * @param key The data store entry's key + * @returns An object containing the entry's key and value + * @throws {DataStoreNotFoundError} An entry with the given key cannot be found + */ + async getEntry(key: string): Promise | undefined> { + const value = this.defaults[key]; + if (value && typeof value === 'object') { + return {key, value}; + } + + if (this.warnOnMissing && !this.warnedKeys.has(key)) { + this.warnedKeys.add(key); + console.warn(`Local data-store provider did not find '${key}'.`); + } + + throw new DataStoreNotFoundError(`Data store entry '${key}' not found.`); + } +} + +function readDefaultsFromEnv(): Record> { + const raw = process.env.SFNEXT_DATA_STORE_DEFAULTS; + if (!raw) { + return {}; + } + + try { + const parsed: unknown = JSON.parse(raw); + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + return parsed as Record>; + } + } catch (error) { + console.warn('Failed to parse SFNEXT_DATA_STORE_DEFAULTS JSON.', error); + } + + return {}; +} + +function readWarnOnMissingFromEnv(): boolean { + const raw = process.env.SFNEXT_DATA_STORE_WARN_ON_MISSING; + if (!raw) { + return true; + } + + return raw.toLowerCase() !== 'false'; +} diff --git a/packages/mrt-utilities/src/data-store/errors.ts b/packages/mrt-utilities/src/data-store/errors.ts new file mode 100644 index 00000000..894b8c61 --- /dev/null +++ b/packages/mrt-utilities/src/data-store/errors.ts @@ -0,0 +1,29 @@ +/* + * 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 + */ + +export class DataStoreNotFoundError extends Error { + constructor(message: string) { + super(message); + this.name = 'DataStoreNotFoundError'; + Object.setPrototypeOf(this, DataStoreNotFoundError.prototype); + } +} + +export class DataStoreServiceError extends Error { + constructor(message: string) { + super(message); + this.name = 'DataStoreServiceError'; + Object.setPrototypeOf(this, DataStoreServiceError.prototype); + } +} + +export class DataStoreUnavailableError extends Error { + constructor(message: string) { + super(message); + this.name = 'DataStoreUnavailableError'; + Object.setPrototypeOf(this, DataStoreUnavailableError.prototype); + } +} diff --git a/packages/mrt-utilities/src/data-store/index.ts b/packages/mrt-utilities/src/data-store/index.ts index 9c854c93..be0ef61f 100644 --- a/packages/mrt-utilities/src/data-store/index.ts +++ b/packages/mrt-utilities/src/data-store/index.ts @@ -4,141 +4,4 @@ * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 */ -import {DynamoDBClient} from '@aws-sdk/client-dynamodb'; -import {DynamoDBDocumentClient, GetCommand, type GetCommandOutput} from '@aws-sdk/lib-dynamodb'; - -import {logMRTError} from '../utils/utils.js'; - -export class DataStoreNotFoundError extends Error { - constructor(message: string) { - super(message); - this.name = 'DataStoreNotFoundError'; - Object.setPrototypeOf(this, DataStoreNotFoundError.prototype); - } -} - -export class DataStoreServiceError extends Error { - constructor(message: string) { - super(message); - this.name = 'DataStoreServiceError'; - Object.setPrototypeOf(this, DataStoreServiceError.prototype); - } -} - -export class DataStoreUnavailableError extends Error { - constructor(message: string) { - super(message); - this.name = 'DataStoreUnavailableError'; - Object.setPrototypeOf(this, DataStoreUnavailableError.prototype); - } -} - -/** - * A class for reading entries from the data store. - * - * This class uses a singleton pattern. - * Use DataStore.getDataStore() to get the singleton instance. - */ -export class DataStore { - private _tableName: string = ''; - private _ddb: DynamoDBDocumentClient | null = null; - private static _instance: DataStore | null = null; - - /** @internal Test hook: inject a document client for unit tests */ - static _testDocumentClient: DynamoDBDocumentClient | null = null; - /** @internal Test hook: inject logMRTError for unit tests */ - static _testLogMRTError: ((namespace: string, err: unknown, context?: Record) => void) | null = null; - - private constructor() { - // Private constructor for singleton; use DataStore.getDataStore() instead. - } - - /** - * Get or create a DynamoDB document client (for abstraction of attribute values). - * - * @private - * @returns The DynamoDB document client - * @throws {DataStoreUnavailableError} The data store is unavailable - */ - private getClient(): DynamoDBDocumentClient { - if (!this.isDataStoreAvailable()) { - throw new DataStoreUnavailableError('The data store is unavailable.'); - } - - if (DataStore._testDocumentClient) { - this._tableName = `DataAccessLayer-${process.env.AWS_REGION}`; - return DataStore._testDocumentClient; - } - - if (!this._ddb) { - this._tableName = `DataAccessLayer-${process.env.AWS_REGION}`; - this._ddb = DynamoDBDocumentClient.from( - new DynamoDBClient({ - region: process.env.AWS_REGION, - }), - ); - } - - return this._ddb; - } - - /** - * Get or create the singleton DataStore instance. - * - * @returns The singleton DataStore instance - */ - static getDataStore(): DataStore { - if (!DataStore._instance) { - DataStore._instance = new DataStore(); - } - return DataStore._instance; - } - - /** - * Whether the data store can be used in the current environment. - * - * @returns true if the data store is available, false otherwise - */ - isDataStoreAvailable(): boolean { - return Boolean(process.env.AWS_REGION && process.env.MOBIFY_PROPERTY_ID && process.env.DEPLOY_TARGET); - } - - /** - * Fetch an entry from the data store. - * - * @param key The data store entry's key - * @returns An object containing the entry's key and value - * @throws {DataStoreUnavailableError} The data store is unavailable - * @throws {DataStoreNotFoundError} An entry with the given key cannot be found - * @throws {DataStoreServiceError} An internal error occurred - */ - async getEntry(key: string): Promise | undefined> { - if (!this.isDataStoreAvailable()) { - throw new DataStoreUnavailableError('The data store is unavailable.'); - } - - const ddb = this.getClient(); - let response: GetCommandOutput; - try { - response = await ddb.send( - new GetCommand({ - TableName: this._tableName, - Key: { - projectEnvironment: `${process.env.MOBIFY_PROPERTY_ID} ${process.env.DEPLOY_TARGET}`, - key, - }, - }), - ); - } catch (error) { - const logFn = DataStore._testLogMRTError ?? logMRTError; - logFn('data_store', error, {key, tableName: this._tableName}); - throw new DataStoreServiceError('Data store request failed.'); - } - - if (!response.Item?.value) { - throw new DataStoreNotFoundError(`Data store entry '${key}' not found.`); - } - - return {key, value: response.Item.value}; - } -} +export * from './production.js'; diff --git a/packages/mrt-utilities/src/data-store/production.ts b/packages/mrt-utilities/src/data-store/production.ts new file mode 100644 index 00000000..1a204fae --- /dev/null +++ b/packages/mrt-utilities/src/data-store/production.ts @@ -0,0 +1,123 @@ +/* + * 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 {DynamoDBClient} from '@aws-sdk/client-dynamodb'; +import {DynamoDBDocumentClient, GetCommand, type GetCommandOutput} from '@aws-sdk/lib-dynamodb'; + +import {DataStoreNotFoundError, DataStoreServiceError, DataStoreUnavailableError} from './errors.js'; +import {logMRTError} from '../utils/utils.js'; + +export {DataStoreNotFoundError, DataStoreServiceError, DataStoreUnavailableError} from './errors.js'; + +/** + * A class for reading entries from the data store. + * + * This class uses a singleton pattern. + * Use DataStore.getDataStore() to get the singleton instance. + */ +export class DataStore { + private _tableName: string = ''; + private _ddb: DynamoDBDocumentClient | null = null; + private static _instance: DataStore | null = null; + + /** @internal Test hook: inject a document client for unit tests */ + static _testDocumentClient: DynamoDBDocumentClient | null = null; + /** @internal Test hook: inject logMRTError for unit tests */ + static _testLogMRTError: ((namespace: string, err: unknown, context?: Record) => void) | null = null; + + private constructor() { + // Private constructor for singleton; use DataStore.getDataStore() instead. + } + + /** + * Get or create a DynamoDB document client (for abstraction of attribute values). + * + * @private + * @returns The DynamoDB document client + * @throws {DataStoreUnavailableError} The data store is unavailable + */ + private getClient(): DynamoDBDocumentClient { + if (!this.isDataStoreAvailable()) { + throw new DataStoreUnavailableError('The data store is unavailable.'); + } + + if (DataStore._testDocumentClient) { + this._tableName = `DataAccessLayer-${process.env.AWS_REGION}`; + return DataStore._testDocumentClient; + } + + if (!this._ddb) { + this._tableName = `DataAccessLayer-${process.env.AWS_REGION}`; + this._ddb = DynamoDBDocumentClient.from( + new DynamoDBClient({ + region: process.env.AWS_REGION, + }), + ); + } + + return this._ddb; + } + + /** + * Get or create the singleton DataStore instance. + * + * @returns The singleton DataStore instance + */ + static getDataStore(): DataStore { + if (!DataStore._instance) { + DataStore._instance = new DataStore(); + } + return DataStore._instance; + } + + /** + * Whether the data store can be used in the current environment. + * + * @returns true if the data store is available, false otherwise + */ + isDataStoreAvailable(): boolean { + return Boolean(process.env.AWS_REGION && process.env.MOBIFY_PROPERTY_ID && process.env.DEPLOY_TARGET); + } + + /** + * Fetch an entry from the data store. + * + * @param key The data store entry's key + * @returns An object containing the entry's key and value + * @throws {DataStoreUnavailableError} The data store is unavailable + * @throws {DataStoreNotFoundError} An entry with the given key cannot be found + * @throws {DataStoreServiceError} An internal error occurred + */ + async getEntry(key: string): Promise | undefined> { + if (!this.isDataStoreAvailable()) { + throw new DataStoreUnavailableError('The data store is unavailable.'); + } + + const ddb = this.getClient(); + let response: GetCommandOutput; + try { + response = await ddb.send( + new GetCommand({ + TableName: this._tableName, + Key: { + projectEnvironment: `${process.env.MOBIFY_PROPERTY_ID} ${process.env.DEPLOY_TARGET}`, + key, + }, + }), + ); + } catch (error) { + const logFn = DataStore._testLogMRTError ?? logMRTError; + logFn('data_store', error, {key, tableName: this._tableName}); + throw new DataStoreServiceError('Data store request failed.'); + } + + if (!response.Item?.value) { + throw new DataStoreNotFoundError(`Data store entry '${key}' not found.`); + } + + return {key, value: response.Item.value}; + } +} diff --git a/packages/mrt-utilities/test/data-store-development.test.ts b/packages/mrt-utilities/test/data-store-development.test.ts new file mode 100644 index 00000000..a7e87543 --- /dev/null +++ b/packages/mrt-utilities/test/data-store-development.test.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 {expect} from 'chai'; +import sinon from 'sinon'; + +import {DataStore, DataStoreNotFoundError} from '../src/data-store/development.js'; + +describe('DataStore (development)', () => { + let originalEnv: NodeJS.ProcessEnv; + + beforeEach(() => { + originalEnv = {...process.env}; + (DataStore as unknown as {_instance: DataStore | null})._instance = null; + }); + + afterEach(() => { + process.env = originalEnv; + (DataStore as unknown as {_instance: DataStore | null})._instance = null; + sinon.restore(); + }); + + it('is always available in development mode', () => { + const store = DataStore.getDataStore(); + expect(store.isDataStoreAvailable()).to.equal(true); + }); + + it('returns entries from SFNEXT_DATA_STORE_DEFAULTS', async () => { + process.env.SFNEXT_DATA_STORE_DEFAULTS = JSON.stringify({ + 'my-key': {theme: 'dark'}, + }); + + const store = DataStore.getDataStore(); + const result = await store.getEntry('my-key'); + + expect(result).to.deep.equal({ + key: 'my-key', + value: {theme: 'dark'}, + }); + }); + + it('throws DataStoreNotFoundError for missing keys by default', async () => { + process.env.SFNEXT_DATA_STORE_DEFAULTS = JSON.stringify({ + 'other-key': {theme: 'dark'}, + }); + + const store = DataStore.getDataStore(); + + try { + await store.getEntry('my-key'); + expect.fail('should have thrown'); + } catch (error) { + expect(error).to.be.an.instanceOf(DataStoreNotFoundError); + expect((error as Error).message).to.include("Data store entry 'my-key' not found"); + } + }); + + it('warns once per missing key when warnings are enabled', async () => { + process.env.SFNEXT_DATA_STORE_DEFAULTS = '{}'; + const warnStub = sinon.stub(console, 'warn'); + const store = DataStore.getDataStore(); + + for (let i = 0; i < 2; i++) { + try { + await store.getEntry('my-key'); + } catch { + // expected + } + } + + expect(warnStub.calledOnce).to.equal(true); + expect(warnStub.firstCall.firstArg).to.include("Local data-store provider did not find 'my-key'"); + }); + + it('does not warn for missing keys when SFNEXT_DATA_STORE_WARN_ON_MISSING=false', async () => { + process.env.SFNEXT_DATA_STORE_DEFAULTS = '{}'; + process.env.SFNEXT_DATA_STORE_WARN_ON_MISSING = 'false'; + + const warnStub = sinon.stub(console, 'warn'); + const store = DataStore.getDataStore(); + + try { + await store.getEntry('my-key'); + expect.fail('should have thrown'); + } catch (error) { + expect(error).to.be.an.instanceOf(DataStoreNotFoundError); + } + + expect(warnStub.called).to.equal(false); + }); + + it('warns when SFNEXT_DATA_STORE_DEFAULTS is invalid JSON', async () => { + process.env.SFNEXT_DATA_STORE_DEFAULTS = '{"my-key": '; + process.env.SFNEXT_DATA_STORE_WARN_ON_MISSING = 'false'; + + const warnStub = sinon.stub(console, 'warn'); + const store = DataStore.getDataStore(); + + try { + await store.getEntry('my-key'); + expect.fail('should have thrown'); + } catch (error) { + expect(error).to.be.an.instanceOf(DataStoreNotFoundError); + } + + expect(warnStub.calledOnce).to.equal(true); + expect(warnStub.firstCall.firstArg).to.equal('Failed to parse SFNEXT_DATA_STORE_DEFAULTS JSON.'); + }); +});