Skip to content

Commit 35e5bfe

Browse files
authored
Merge pull request #149 from devsapp/feat/list-auto-pagination
feat: list command defaults to auto-pagination when --limit is not sp…
2 parents 1586992 + 6adf276 commit 35e5bfe

8 files changed

Lines changed: 377 additions & 3 deletions

File tree

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ __tests__/e2e/python/code/apt-archives
6060
__tests__/e2e/apt/code/package-lock.json
6161

6262
.env_test
63+
.env
6364

6465
CLAUDE.md
6566
agent-prompt.md
66-
AGENTS.md
67+
AGENTS.md
68+
.qoder
69+
docs/

__tests__/ut/commands/list_test.ts

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import List from '../../../src/subCommands/list';
2+
import FC from '../../../src/resources/fc';
3+
import { IInputs } from '../../../src/interface';
4+
import { tableShow } from '../../../src/utils';
5+
6+
// Mock dependencies
7+
jest.mock('../../../src/resources/fc');
8+
jest.mock('../../../src/logger', () => ({
9+
_set: jest.fn(),
10+
debug: jest.fn(),
11+
info: jest.fn(),
12+
error: jest.fn(),
13+
warn: jest.fn(),
14+
log: jest.fn(),
15+
}));
16+
jest.mock('../../../src/utils', () => ({
17+
tableShow: jest.fn(),
18+
isAppCenter: jest.fn(),
19+
getUserAgent: jest.fn((userAgent, command) => {
20+
return (
21+
userAgent ||
22+
`Component:fc3;Nodejs:${process.version};OS:${process.platform}-${process.arch};command:${command}`
23+
);
24+
}),
25+
}));
26+
27+
describe('List', () => {
28+
let list: List;
29+
let mockInputs: IInputs;
30+
let mockFcSdk: jest.Mocked<FC>;
31+
32+
const mockFunctionsArray = [
33+
{
34+
functionName: 'test-func-1',
35+
runtime: 'nodejs18',
36+
handler: 'index.handler',
37+
memorySize: 128,
38+
state: 'Active',
39+
lastModifiedTime: '2024-01-01T00:00:00Z',
40+
},
41+
{
42+
functionName: 'test-func-2',
43+
runtime: 'python3.10',
44+
handler: 'main.handler',
45+
memorySize: 256,
46+
state: 'Active',
47+
lastModifiedTime: '2024-01-02T00:00:00Z',
48+
},
49+
];
50+
51+
const mockFunctionsPageBody = {
52+
functions: mockFunctionsArray,
53+
nextToken: 'next-token-123',
54+
};
55+
56+
beforeEach(() => {
57+
mockInputs = {
58+
props: {
59+
region: 'cn-hangzhou',
60+
},
61+
credential: {
62+
AccountID: 'test-account',
63+
AccessKeyID: 'test-access-key-id',
64+
AccessKeySecret: 'test-access-key-secret',
65+
},
66+
args: [],
67+
} as any;
68+
69+
mockFcSdk = new FC(mockInputs.props.region, mockInputs.credential, {}) as jest.Mocked<FC>;
70+
(FC as unknown as jest.Mock).mockImplementation(() => mockFcSdk);
71+
});
72+
73+
afterEach(() => {
74+
jest.clearAllMocks();
75+
});
76+
77+
describe('constructor', () => {
78+
it('should create List instance with valid inputs', () => {
79+
mockInputs.args = [];
80+
list = new List(mockInputs);
81+
expect(list).toBeInstanceOf(List);
82+
});
83+
84+
it('should throw error when region is not specified', () => {
85+
delete mockInputs.props.region;
86+
mockInputs.args = [];
87+
expect(() => new List(mockInputs)).toThrow('Region not specified, please specify --region');
88+
});
89+
90+
it('should use region from command line args over props', () => {
91+
mockInputs.args = ['--region', 'cn-beijing'];
92+
list = new List(mockInputs);
93+
expect(list).toBeInstanceOf(List);
94+
});
95+
});
96+
97+
describe('run - without limit (auto-pagination)', () => {
98+
beforeEach(() => {
99+
mockInputs.args = [];
100+
list = new List(mockInputs);
101+
});
102+
103+
it('should list all functions via auto-pagination when no limit specified', async () => {
104+
mockFcSdk.listFunctions = jest.fn().mockResolvedValue(mockFunctionsArray);
105+
106+
const result = await list.run();
107+
expect(mockFcSdk.listFunctions).toHaveBeenCalledWith(undefined);
108+
expect(mockFcSdk.listFunctionsPage).not.toHaveBeenCalled();
109+
expect(result).toEqual({ functions: mockFunctionsArray });
110+
});
111+
112+
it('should list all functions with prefix filter via auto-pagination', async () => {
113+
mockInputs.args = ['--prefix', 'test'];
114+
list = new List(mockInputs);
115+
mockFcSdk.listFunctions = jest.fn().mockResolvedValue(mockFunctionsArray);
116+
117+
const result = await list.run();
118+
expect(mockFcSdk.listFunctions).toHaveBeenCalledWith('test');
119+
expect(result).toEqual({ functions: mockFunctionsArray });
120+
});
121+
122+
it('should show table output without limit', async () => {
123+
mockInputs.args = ['--table'];
124+
list = new List(mockInputs);
125+
mockFcSdk.listFunctions = jest.fn().mockResolvedValue(mockFunctionsArray);
126+
127+
await list.run();
128+
expect(tableShow).toHaveBeenCalledWith(mockFunctionsArray, [
129+
'functionName',
130+
'runtime',
131+
'handler',
132+
'memorySize',
133+
'state',
134+
'lastModifiedTime',
135+
]);
136+
});
137+
138+
it('should handle empty functions list via auto-pagination', async () => {
139+
mockFcSdk.listFunctions = jest.fn().mockResolvedValue([]);
140+
141+
const result = await list.run();
142+
expect(result).toEqual({ functions: [] });
143+
});
144+
145+
it('should not call tableShow when --table is not specified', async () => {
146+
mockFcSdk.listFunctions = jest.fn().mockResolvedValue(mockFunctionsArray);
147+
148+
await list.run();
149+
expect(tableShow).not.toHaveBeenCalled();
150+
});
151+
});
152+
153+
describe('run - with limit (single page)', () => {
154+
it('should list functions with custom limit via single page', async () => {
155+
mockInputs.args = ['--limit', '20'];
156+
list = new List(mockInputs);
157+
mockFcSdk.listFunctionsPage = jest.fn().mockResolvedValue(mockFunctionsPageBody);
158+
159+
const result = await list.run();
160+
expect(mockFcSdk.listFunctionsPage).toHaveBeenCalledWith(20, undefined, undefined);
161+
expect(mockFcSdk.listFunctions).not.toHaveBeenCalled();
162+
expect(result).toEqual(mockFunctionsPageBody);
163+
});
164+
165+
it('should reject --limit with value 0', async () => {
166+
mockInputs.args = ['--limit', '0'];
167+
list = new List(mockInputs);
168+
169+
await expect(list.run()).rejects.toThrow('--limit must be a positive integer');
170+
});
171+
172+
it('should reject --limit with non-integer value', async () => {
173+
mockInputs.args = ['--limit', 'abc'];
174+
list = new List(mockInputs);
175+
176+
await expect(list.run()).rejects.toThrow('--limit must be a positive integer');
177+
});
178+
179+
it('should list functions with prefix and limit', async () => {
180+
mockInputs.args = ['--prefix', 'test', '--limit', '20'];
181+
list = new List(mockInputs);
182+
mockFcSdk.listFunctionsPage = jest.fn().mockResolvedValue(mockFunctionsPageBody);
183+
184+
const result = await list.run();
185+
expect(mockFcSdk.listFunctionsPage).toHaveBeenCalledWith(20, 'test', undefined);
186+
expect(result).toEqual(mockFunctionsPageBody);
187+
});
188+
189+
it('should list functions with nextToken for pagination', async () => {
190+
mockInputs.args = ['--limit', '20', '--next-token', 'next-token-123'];
191+
list = new List(mockInputs);
192+
mockFcSdk.listFunctionsPage = jest.fn().mockResolvedValue(mockFunctionsPageBody);
193+
194+
const result = await list.run();
195+
expect(mockFcSdk.listFunctionsPage).toHaveBeenCalledWith(20, undefined, 'next-token-123');
196+
expect(result).toEqual(mockFunctionsPageBody);
197+
});
198+
199+
it('should list functions with all query parameters', async () => {
200+
mockInputs.args = ['--prefix', 'test', '--limit', '10', '--next-token', 'token-abc'];
201+
list = new List(mockInputs);
202+
mockFcSdk.listFunctionsPage = jest.fn().mockResolvedValue(mockFunctionsPageBody);
203+
204+
const result = await list.run();
205+
expect(mockFcSdk.listFunctionsPage).toHaveBeenCalledWith(10, 'test', 'token-abc');
206+
expect(result).toEqual(mockFunctionsPageBody);
207+
});
208+
209+
it('should show table output with limit', async () => {
210+
mockInputs.args = ['--limit', '20', '--table'];
211+
list = new List(mockInputs);
212+
mockFcSdk.listFunctionsPage = jest.fn().mockResolvedValue(mockFunctionsPageBody);
213+
214+
await list.run();
215+
expect(tableShow).toHaveBeenCalledWith(mockFunctionsArray, [
216+
'functionName',
217+
'runtime',
218+
'handler',
219+
'memorySize',
220+
'state',
221+
'lastModifiedTime',
222+
]);
223+
});
224+
225+
it('should handle table output with empty functions via single page', async () => {
226+
mockInputs.args = ['--limit', '20', '--table'];
227+
list = new List(mockInputs);
228+
mockFcSdk.listFunctionsPage = jest.fn().mockResolvedValue({
229+
functions: [],
230+
});
231+
232+
await list.run();
233+
expect(tableShow).toHaveBeenCalledWith(
234+
[],
235+
['functionName', 'runtime', 'handler', 'memorySize', 'state', 'lastModifiedTime'],
236+
);
237+
});
238+
});
239+
240+
describe('run - error handling', () => {
241+
it('should propagate auto-pagination API errors', async () => {
242+
mockInputs.args = [];
243+
list = new List(mockInputs);
244+
mockFcSdk.listFunctions = jest.fn().mockRejectedValue(new Error('API error'));
245+
246+
await expect(list.run()).rejects.toThrow('API error');
247+
});
248+
249+
it('should propagate single page API errors', async () => {
250+
mockInputs.args = ['--limit', '20'];
251+
list = new List(mockInputs);
252+
mockFcSdk.listFunctionsPage = jest.fn().mockRejectedValue(new Error('API error'));
253+
254+
await expect(list.run()).rejects.toThrow('API error');
255+
});
256+
});
257+
});

publish.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Type: Component
33
Name: fc3
44
Provider:
55
- 阿里云
6-
Version: 0.1.18
6+
Version: 0.1.19
77
Description: 阿里云函数计算全生命周期管理
88
HomePage: https://github.com/devsapp/fc3
99
Organization: 阿里云函数计算(FC)

src/commands-help/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import s2tos3 from './s2tos3';
1616
import logs from './logs';
1717
import session from './session';
1818
import scaling from './scaling';
19+
import list from './list';
1920

2021
export default {
2122
deploy,
@@ -36,4 +37,5 @@ export default {
3637
logs,
3738
session,
3839
scaling,
40+
list,
3941
};

src/commands-help/list.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export default {
2+
help: {
3+
description: `List all functions.
4+
Example:
5+
$ s list
6+
$ s list --prefix test --table
7+
$ s cli fc3 list --prefix test --limit 20 --next-token xxx --region cn-hangzhou -a default`,
8+
summary: 'List all functions',
9+
option: [
10+
[
11+
'--region <region>',
12+
'[C-Required] Specify the fc region, you can see all supported regions in https://help.aliyun.com/document_detail/2512917.html',
13+
],
14+
['--prefix <prefix>', '[Optional] Specify the prefix of function name'],
15+
[
16+
'--limit <limit>',
17+
'[Optional] Specify the max number of functions to return per page, if not specified, all functions will be listed',
18+
],
19+
[
20+
'--next-token <nextToken>',
21+
'[Optional] Specify the next token for pagination, only works with --limit',
22+
],
23+
['--table', '[Optional] Specify if output the result as table format'],
24+
],
25+
},
26+
};

src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import Alias from './subCommands/alias';
2525
import Concurrency from './subCommands/concurrency';
2626
import SYaml2To3 from './subCommands/2to3';
2727
import Logs from './subCommands/logs';
28+
import List from './subCommands/list';
2829
import { SCHEMA_FILE_PATH } from './constant';
2930
import { checkDockerIsOK, isAppCenter, isYunXiao } from './utils';
3031
import { Model } from './subCommands/model';
@@ -183,6 +184,12 @@ export default class Fc extends Base {
183184
return await logs.run();
184185
}
185186

187+
public async list(inputs: IInputs) {
188+
await super.handlePreRun(inputs, true);
189+
const list = new List(inputs);
190+
return await list.run();
191+
}
192+
186193
public async model(inputs: IInputs) {
187194
await super.handlePreRun(inputs, false);
188195
const model = new Model(inputs);

src/resources/fc/impl/client.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,12 +418,21 @@ export default class FC_Client {
418418
return body;
419419
}
420420

421+
async listFunctionsPage(limit: number, prefix?: string, nextToken?: string) {
422+
const request = new ListFunctionsRequest({ limit, prefix, nextToken });
423+
const runtime = new RuntimeOptions({});
424+
const headers: { [key: string]: string } = {};
425+
const result = await this.fc20230330Client.listFunctionsWithOptions(request, headers, runtime);
426+
const { body } = result.toMap();
427+
return body;
428+
}
429+
421430
/**
422431
* list 接口实现模版
423432
*/
424433
async listFunctions(prefix?: string): Promise<any[]> {
425434
let nextToken = '';
426-
const limit = 2;
435+
const limit = 100;
427436
const functions: any[] = [];
428437

429438
while (true) {

0 commit comments

Comments
 (0)