Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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\\)`"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 16 additions & 4 deletions src/transport/cma-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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 () => {
Expand Down
30 changes: 23 additions & 7 deletions src/transport/cma-client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { ContentstackClient } from '@contentstack/cli-utilities';

import { UsageError } from '../core/errors';
import {
configHandler,
managementSDKClient,
Expand All @@ -15,20 +17,30 @@ export interface CmaCollection {
const PAGE_SIZE = 100;
const MAX_PAGES = 100;

async function allPages(fetchPage: (skip: number) => Promise<CmaCollection>): Promise<CmaCollection> {
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<CmaCollection>,
subject: string,
): Promise<CmaCollection> {
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 {
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/variables/variable.parse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
4 changes: 2 additions & 2 deletions src/variables/variable.parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading