diff --git a/.talismanrc b/.talismanrc index 6904212..918dfb0 100644 --- a/.talismanrc +++ b/.talismanrc @@ -10,7 +10,7 @@ fileignoreconfig: - filename: src/transport/rest-client.test.ts checksum: 3b5751449c61f7a1d71b978ed03b9472f714e12dd1679f691bbd4dd8f84be923 - filename: src/transport/cma-client.ts - checksum: 87d8c6f8a41ea520ecc5fb4b6f71467d44af95f55d1facec1de4778fe22677b3 + checksum: 0e144de22c757e8d3efd31c892d6de56796a49ce2c18d0e41ba16eff8dc3d24e - filename: src/transport/auth-strategy.test.ts checksum: a4f3e3ea7db10dbf82abbac0207d2231ee740916f476fcda836a2268505295ef - filename: src/projects/types.ts @@ -233,7 +233,7 @@ fileignoreconfig: - "a project name passed on argv to the project resolver" - "expect\\(Object.keys\\(DEPENDENCIES\\)\\).toEqual\\(\\['project', 'env'\\]\\)" - filename: AGENTS.md - checksum: 66b94ec2bcb34947bcda15e6364dceee7edb50bfe851e26e6c737425315e81da + checksum: 4766a4bf28eab95d90878b825979cef70c83a79a24fb9450a52d617ce0933f78 allowed_patterns: - "Do not add an exemption to a guard to make a new file pass" - "passes its own error wording to `client.request\\(req, MESSAGES\\)`" @@ -303,7 +303,7 @@ fileignoreconfig: - filename: src/functions/function.logger.test.ts checksum: 840b2b2b7f938e0862881871e8d568112631c58e12cf6c8a5be32e7552b8cf9e - filename: src/transport/cma-client.test.ts - checksum: 9eb4958cd7fa07822954d14c5860c5ca18f0880e4390716ebfa9c826add49311 + checksum: 471361febb8e08a50af8cf7e8f81824adc7a31af6231d78baf0e125823f43686 - filename: src/core/catalog.test.ts checksum: bf8c203a8efc40a75f5407ceb29a639ea9a6e1fa081d02260def2d7e0fab958a - filename: test/credential-guard.setup.ts @@ -373,9 +373,9 @@ fileignoreconfig: - filename: src/variables/variable.page.test.ts checksum: 9a4e63c0dbfd35b499149a98ce5642787097e9491746b4b874b2eda88f1d2b50 - filename: src/variables/variable.parse.test.ts - checksum: 01441ffca77438dbd29420ad38fbfee160dfa535434e4a3eca1c57c47eeebaa0 + checksum: d620d89c4b5cafb80dcf696f430c8fa4e696dfc744f814bcdd74324243010460 - filename: src/variables/variable.parse.ts - checksum: 9dbb9e0c8715e319aa70a4b6a49ae255a3a89207081f6bff0f4c21ea91eed7e7 + checksum: 87be9f454ce0ffb48ff5729ccd3c1b105ce9f1030b32278c3359555b983c2142 - filename: src/variables/variable.presenter.test.ts checksum: 62e8b0eff16ed4408d929092ce6808aeabea001a6cc15780d46c4ac4226eec3d - filename: src/variables/variable.presenter.ts diff --git a/AGENTS.md b/AGENTS.md index ec8e060..339578d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -343,7 +343,10 @@ under `--data-dir`, always printing the file it chose, and fails with exit 2 whe exit 2 naming `--dotenv -` for two or more. The API refuses an empty value, so `KEY=` is a usage error naming the key, and an empty value inside a dotenv file is skipped with its key reported. No value reaches stdout, a prompt message, a confirmation or an error: `list` masks through `redactedColumn`, and a failed `PATCH` is worded from -`ENVIRONMENT_ERROR_MESSAGES`, never from the API's own text. +`ENVIRONMENT_ERROR_MESSAGES`. A code with no entry there falls back to the API's own message, which the +service words from keys at most, never from a value (CL-7179). The stack and delivery-token lookups behind +`--from-stack` walk the Management API 100 per page and raise `CmaScanLimitError` after `MAX_PAGES` pages rather +than return a truncated list. `projects:create` and `env:create` take `--var` and `--dotenv` and reach the same resolver through `EnvironmentBuilder`, which asks last - after Contentstack Authentication, where V1 asked - with no auto-detection. diff --git a/src/transport/cma-client.test.ts b/src/transport/cma-client.test.ts index 2ff7f7e..2c22e04 100644 --- a/src/transport/cma-client.test.ts +++ b/src/transport/cma-client.test.ts @@ -209,7 +209,7 @@ describe('createCmaSession', () => { ]); }); - it('gives up after a bounded number of pages when the server keeps answering', async () => { + it('raises after a bounded number of pages when the server keeps answering, rather than dropping the rest', async () => { let calls = 0; sdkClient.mockReset(); sdkClient.mockResolvedValue({ @@ -224,10 +224,22 @@ describe('createCmaSession', () => { }); const session = createCmaSession({ cma: 'https://api.contentstack.io', analyticsInfo: 'cli/2.0.0' }); - const { items } = await session.fetchStacks('org7'); - + await expect(session.fetchStacks('org7')).rejects.toThrow( + 'Stopped after fetching 100 pages of stacks without reaching the end of the list.', + ); expect(calls).toBe(100); - expect(items).toHaveLength(100); + }); + + it('names delivery tokens when it is their list that cannot be finished', async () => { + sdkClient.mockReset(); + sdkClient.mockResolvedValue({ + stack: () => ({ deliveryToken: () => ({ query: () => ({ find: async () => ({ items: [{ token: 'same' }] }) }) }) }), + }); + const session = createCmaSession({ cma: 'https://api.contentstack.io', analyticsInfo: 'cli/2.0.0' }); + + await expect(session.fetchDeliveryTokens('blt1')).rejects.toThrow( + 'Stopped after fetching 100 pages of delivery tokens without reaching the end of the list.', + ); }); it('treats a reply with no items as an empty list', async () => { diff --git a/src/transport/cma-client.ts b/src/transport/cma-client.ts index 1339b77..50d418c 100644 --- a/src/transport/cma-client.ts +++ b/src/transport/cma-client.ts @@ -1,4 +1,6 @@ import type { ContentstackClient } from '@contentstack/cli-utilities'; + +import { UsageError } from '../core/errors'; import { configHandler, managementSDKClient, @@ -15,20 +17,30 @@ export interface CmaCollection { const PAGE_SIZE = 100; const MAX_PAGES = 100; -async function allPages(fetchPage: (skip: number) => Promise): Promise { +export class CmaScanLimitError extends UsageError { + constructor(subject: string) { + super(`Stopped after fetching ${MAX_PAGES} pages of ${subject} without reaching the end of the list.`); + this.name = 'CmaScanLimitError'; + } +} + +async function allPages( + fetchPage: (skip: number) => Promise, + subject: string, +): Promise { const items: unknown[] = []; for (let page = 0; page < MAX_PAGES; page += 1) { const batch = (await fetchPage(items.length)).items ?? []; if (batch.length === 0) { - break; + return { items }; } items.push(...batch); } - return { items }; + throw new CmaScanLimitError(subject); } export interface CmaSession { @@ -71,12 +83,16 @@ export function createCmaSession(options: CmaSessionOptions): CmaSession { fetchOrganizations: async (query) => (await open()).organization().fetchAll(query), fetchOrganization: async (uid) => (await open()).organization(uid).fetch(), fetchStacks: (organizationUid) => - allPages(async (skip) => - (await open()).stack().query({ organization_uid: organizationUid, limit: PAGE_SIZE, skip }).find(), + allPages( + async (skip) => + (await open()).stack().query({ organization_uid: organizationUid, limit: PAGE_SIZE, skip }).find(), + 'stacks', ), fetchDeliveryTokens: (apiKey) => - allPages(async (skip) => - (await open()).stack({ api_key: apiKey }).deliveryToken().query({ limit: PAGE_SIZE, skip }).find(), + allPages( + async (skip) => + (await open()).stack({ api_key: apiKey }).deliveryToken().query({ limit: PAGE_SIZE, skip }).find(), + 'delivery tokens', ), scopedOrganizationUid: () => { const uid: unknown = configHandler.get(OAUTH_ORG_UID_CONFIG_FIELD); diff --git a/src/variables/variable.parse.test.ts b/src/variables/variable.parse.test.ts index f0f64da..e4604e2 100644 --- a/src/variables/variable.parse.test.ts +++ b/src/variables/variable.parse.test.ts @@ -18,9 +18,10 @@ describe('variable parse', () => { expect(parseAssignment('API_KEY')).toEqual({ key: 'API_KEY', value: undefined }); }); - it('accepts a colon as the separator only when no equals sign is present', () => { + it('splits on whichever of the equals sign and the colon comes first, so the value may hold the other', () => { expect(parseAssignment('API_URL:value')).toEqual({ key: 'API_URL', value: 'value' }); expect(parseAssignment('API_URL=https://x.test:8080')).toEqual({ key: 'API_URL', value: 'https://x.test:8080' }); + expect(parseAssignment('API_URL:https://x.test?a=b')).toEqual({ key: 'API_URL', value: 'https://x.test?a=b' }); }); it('trims the key but not the value', () => { diff --git a/src/variables/variable.parse.ts b/src/variables/variable.parse.ts index d1ff227..52ee7d1 100644 --- a/src/variables/variable.parse.ts +++ b/src/variables/variable.parse.ts @@ -6,9 +6,9 @@ export interface Assignment { } function separatorIndex(token: string): number { - const equals = token.indexOf('='); + const separators = [token.indexOf('='), token.indexOf(':')].filter((index) => index !== -1); - return equals === -1 ? token.indexOf(':') : equals; + return separators.length === 0 ? -1 : Math.min(...separators); } export function parseAssignment(token: string): Assignment {