Skip to content

Commit c7d77e1

Browse files
authored
feat: datasets / key-value-stores commands (#685)
create cmds ![Code - 2024-10-23 at 12 30 27@2x](https://github.com/user-attachments/assets/1e49fc1a-d5a4-44d5-bc84-023610f92ff8) rm cmds (ignore the missing space, it has been fixed after taking the screenshot) ![Code - 2024-10-23 at 12 43 17@2x](https://github.com/user-attachments/assets/abbdc70b-523f-4469-8942-a6f83a16fb64) rename cmds ![Code - 2024-10-23 at 13 13 05@2x](https://github.com/user-attachments/assets/5c0bd490-ff1b-4802-b853-8fddc0b73197)
1 parent 1747146 commit c7d77e1

File tree

16 files changed

+721
-13
lines changed

16 files changed

+721
-13
lines changed

.github/workflows/pre_release.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ jobs:
6868
with:
6969
cache: yarn
7070

71-
- name: Update package version in package.json
72-
run: yarn version ${{ needs.release_metadata.outputs.version_number }}
71+
# - name: Update package version in package.json
72+
# run: yarn version ${{ needs.release_metadata.outputs.version_number }}
7373

7474
- name: Update CHANGELOG.md
7575
uses: DamianReeves/write-file-action@master

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "apify-cli",
3-
"version": "0.20.8",
3+
"version": "0.21.0",
44
"description": "Apify command-line interface (CLI) helps you manage the Apify cloud platform and develop, build, and deploy Apify Actors.",
55
"exports": "./dist/index.js",
66
"types": "./dist/index.d.ts",

src/commands/builds/info.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export class BuildInfoCommand extends ApifyCommand<typeof BuildInfoCommand> {
8686
const dockerImageSize = Reflect.get(build.stats ?? {}, 'imageSizeBytes') as number | undefined;
8787

8888
if (dockerImageSize) {
89-
message.push(` ${chalk.yellow('Docker Image Size')}: ${prettyPrintBytes(dockerImageSize)}`);
89+
message.push(` ${chalk.yellow('Docker Image Size')}: ${prettyPrintBytes({ bytes: dockerImageSize })}`);
9090
}
9191

9292
message.push(` ${chalk.yellow('Origin')}: ${build.meta.origin ?? 'UNKNOWN'}`);

src/commands/datasets/create.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Args } from '@oclif/core';
2+
import chalk from 'chalk';
3+
4+
import { ApifyCommand } from '../../lib/apify_command.js';
5+
import { tryToGetDataset } from '../../lib/commands/storages.js';
6+
import { error, success } from '../../lib/outputs.js';
7+
import { getLoggedClientOrThrow } from '../../lib/utils.js';
8+
9+
export class DatasetsCreateCommand extends ApifyCommand<typeof DatasetsCreateCommand> {
10+
static override description = 'Creates a new Dataset on your account';
11+
12+
static override args = {
13+
datasetName: Args.string({
14+
description: 'Optional name for the Dataset',
15+
required: false,
16+
}),
17+
};
18+
19+
static override enableJsonFlag = true;
20+
21+
async run() {
22+
const { datasetName } = this.args;
23+
24+
const client = await getLoggedClientOrThrow();
25+
26+
if (datasetName) {
27+
const existing = await tryToGetDataset(client, datasetName);
28+
29+
if (existing) {
30+
error({ message: 'A Dataset with this name already exists!' });
31+
return;
32+
}
33+
}
34+
35+
const newDataset = await client.datasets().getOrCreate(datasetName);
36+
37+
if (this.flags.json) {
38+
return newDataset;
39+
}
40+
41+
success({
42+
message: `Dataset with ID ${chalk.yellow(newDataset.id)}${datasetName ? ` (called ${chalk.yellow(datasetName)})` : ''} was created.`,
43+
stdout: true,
44+
});
45+
46+
return undefined;
47+
}
48+
}

src/commands/datasets/ls.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { Flags } from '@oclif/core';
2+
import chalk from 'chalk';
3+
4+
import { ApifyCommand } from '../../lib/apify_command.js';
5+
import { prettyPrintBytes } from '../../lib/commands/pretty-print-bytes.js';
6+
import { CompactMode, ResponsiveTable } from '../../lib/commands/responsive-table.js';
7+
import { info, simpleLog } from '../../lib/outputs.js';
8+
import { getLocalUserInfo, getLoggedClientOrThrow, TimestampFormatter } from '../../lib/utils.js';
9+
10+
const table = new ResponsiveTable({
11+
allColumns: ['Dataset ID', 'Name', 'Items', 'Size', 'Created', 'Modified'],
12+
mandatoryColumns: ['Dataset ID', 'Name', 'Items', 'Size'],
13+
columnAlignments: {
14+
Items: 'right',
15+
},
16+
});
17+
18+
export class DatasetsLsCommand extends ApifyCommand<typeof DatasetsLsCommand> {
19+
static override description = 'Lists all Datasets on your account.';
20+
21+
static override flags = {
22+
offset: Flags.integer({
23+
description: 'Number of Datasets that will be skipped.',
24+
default: 0,
25+
}),
26+
limit: Flags.integer({
27+
description: 'Number of Datasets that will be listed.',
28+
default: 20,
29+
}),
30+
desc: Flags.boolean({
31+
description: 'Sorts Datasets in descending order.',
32+
default: false,
33+
}),
34+
unnamed: Flags.boolean({
35+
description: "Lists Datasets that don't have a name set.",
36+
default: false,
37+
}),
38+
};
39+
40+
static override enableJsonFlag = true;
41+
42+
async run() {
43+
const { desc, offset, limit, json, unnamed } = this.flags;
44+
45+
const client = await getLoggedClientOrThrow();
46+
const user = await getLocalUserInfo();
47+
48+
const rawDatasetList = await client.datasets().list({ desc, offset, limit, unnamed });
49+
50+
if (json) {
51+
return rawDatasetList;
52+
}
53+
54+
if (rawDatasetList.count === 0) {
55+
info({
56+
message: "You don't have any Datasets on your account",
57+
stdout: true,
58+
});
59+
60+
return;
61+
}
62+
63+
for (const dataset of rawDatasetList.items) {
64+
// TODO: update apify-client types
65+
const size = Reflect.get(dataset.stats, 's3StorageBytes') as number | undefined;
66+
67+
table.pushRow({
68+
'Dataset ID': dataset.id,
69+
Created: TimestampFormatter.display(dataset.createdAt),
70+
Items: `${dataset.itemCount}`,
71+
Modified: TimestampFormatter.display(dataset.modifiedAt),
72+
Name: dataset.name ? `${user.username!}/${dataset.name}` : '',
73+
Size:
74+
typeof size === 'number'
75+
? prettyPrintBytes({ bytes: size, shortBytes: true, colorFunc: chalk.gray, precision: 0 })
76+
: chalk.gray('N/A'),
77+
});
78+
}
79+
80+
simpleLog({
81+
message: table.render(CompactMode.WebLikeCompact),
82+
stdout: true,
83+
});
84+
85+
return undefined;
86+
}
87+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Args } from '@oclif/core';
2+
import type { ApifyApiError } from 'apify-client';
3+
import chalk from 'chalk';
4+
5+
import { ApifyCommand } from '../../lib/apify_command.js';
6+
import { tryToGetDataset } from '../../lib/commands/storages.js';
7+
import { error, success } from '../../lib/outputs.js';
8+
import { getLoggedClientOrThrow } from '../../lib/utils.js';
9+
10+
export class DatasetsPushDataCommand extends ApifyCommand<typeof DatasetsPushDataCommand> {
11+
static override description = 'Pushes an object or an array of objects to the provided Dataset.';
12+
13+
static override args = {
14+
nameOrId: Args.string({
15+
required: true,
16+
description: 'The Dataset ID or name to push the objects to',
17+
ignoreStdin: true,
18+
}),
19+
item: Args.string({
20+
required: true,
21+
description: 'The object or array of objects to be pushed.',
22+
}),
23+
};
24+
25+
async run() {
26+
const { nameOrId, item } = this.args;
27+
28+
const client = await getLoggedClientOrThrow();
29+
const existingDataset = await tryToGetDataset(client, nameOrId);
30+
31+
if (!existingDataset) {
32+
error({
33+
message: `Dataset with ID or name "${nameOrId}" not found.`,
34+
});
35+
36+
return;
37+
}
38+
39+
const { datasetClient, dataset } = existingDataset;
40+
41+
let parsedData: Record<string, unknown> | Array<Record<string, unknown>>;
42+
43+
try {
44+
parsedData = JSON.parse(item);
45+
} catch (err) {
46+
error({
47+
message: `Failed to parse data as JSON string: ${(err as Error).message}`,
48+
});
49+
50+
return;
51+
}
52+
53+
if (Array.isArray(parsedData) && parsedData.length === 0) {
54+
error({
55+
message: 'No items were provided.',
56+
});
57+
return;
58+
}
59+
60+
const idMessage = dataset.name
61+
? `Dataset named ${chalk.yellow(dataset.name)} (${chalk.gray('ID:')} ${chalk.yellow(dataset.id)})`
62+
: `Dataset with ID ${chalk.yellow(dataset.id)}`;
63+
64+
try {
65+
await datasetClient.pushItems(parsedData);
66+
67+
success({
68+
message: `${this.pluralString(Array.isArray(parsedData) ? parsedData.length : 1, 'Object', 'Objects')} pushed to ${idMessage} successfully.`,
69+
});
70+
} catch (err) {
71+
const casted = err as ApifyApiError;
72+
73+
error({
74+
message: `Failed to push items into ${idMessage}\n ${casted.message || casted}`,
75+
});
76+
}
77+
}
78+
}

src/commands/datasets/rename.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { Args, Flags } from '@oclif/core';
2+
import type { ApifyApiError } from 'apify-client';
3+
import chalk from 'chalk';
4+
5+
import { ApifyCommand } from '../../lib/apify_command.js';
6+
import { tryToGetDataset } from '../../lib/commands/storages.js';
7+
import { error, success } from '../../lib/outputs.js';
8+
import { getLoggedClientOrThrow } from '../../lib/utils.js';
9+
10+
export class DatasetsRenameCommand extends ApifyCommand<typeof DatasetsRenameCommand> {
11+
static override description = 'Renames a Dataset, or removes its unique name';
12+
13+
static override flags = {
14+
unname: Flags.boolean({
15+
description: 'Removes the unique name of the Dataset',
16+
}),
17+
};
18+
19+
static override args = {
20+
nameOrId: Args.string({
21+
description: 'The Dataset ID or name to delete',
22+
required: true,
23+
}),
24+
newName: Args.string({
25+
description: 'The new name for the Dataset',
26+
}),
27+
};
28+
29+
async run() {
30+
const { unname } = this.flags;
31+
const { newName, nameOrId } = this.args;
32+
33+
if (!newName && !unname) {
34+
error({ message: 'You must provide either a new name or the --unname flag.' });
35+
return;
36+
}
37+
38+
if (newName && unname) {
39+
error({
40+
message: 'You cannot provide a new name and the --unname flag.',
41+
});
42+
return;
43+
}
44+
45+
const client = await getLoggedClientOrThrow();
46+
const existingDataset = await tryToGetDataset(client, nameOrId);
47+
48+
if (!existingDataset) {
49+
error({
50+
message: `Dataset with ID or name "${nameOrId}" not found.`,
51+
});
52+
53+
return;
54+
}
55+
56+
const { id, name } = existingDataset.dataset;
57+
58+
const successMessage = (() => {
59+
if (!name) {
60+
return `The name of the Dataset with ID ${chalk.yellow(id)} has been set to: ${chalk.yellow(newName)}`;
61+
}
62+
63+
if (unname) {
64+
return `The name of the Dataset with ID ${chalk.yellow(id)} has been removed (was ${chalk.yellow(name)} previously).`;
65+
}
66+
67+
return `The name of the Dataset with ID ${chalk.yellow(id)} was changed from ${chalk.yellow(name)} to ${chalk.yellow(newName)}.`;
68+
})();
69+
70+
try {
71+
await existingDataset.datasetClient.update({ name: unname ? (null as never) : newName! });
72+
73+
success({
74+
message: successMessage,
75+
stdout: true,
76+
});
77+
} catch (err) {
78+
const casted = err as ApifyApiError;
79+
80+
error({
81+
message: `Failed to rename Dataset with ID ${chalk.yellow(id)}\n ${casted.message || casted}`,
82+
});
83+
}
84+
}
85+
}

src/commands/datasets/rm.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { Args } from '@oclif/core';
2+
import type { ApifyApiError } from 'apify-client';
3+
import chalk from 'chalk';
4+
5+
import { ApifyCommand } from '../../lib/apify_command.js';
6+
import { confirmAction } from '../../lib/commands/confirm.js';
7+
import { tryToGetDataset } from '../../lib/commands/storages.js';
8+
import { error, info, success } from '../../lib/outputs.js';
9+
import { getLoggedClientOrThrow } from '../../lib/utils.js';
10+
11+
export class DatasetsRmCommand extends ApifyCommand<typeof DatasetsRmCommand> {
12+
static override description = 'Deletes a Dataset';
13+
14+
static override args = {
15+
datasetNameOrId: Args.string({
16+
description: 'The Dataset ID or name to delete',
17+
required: true,
18+
}),
19+
};
20+
21+
async run() {
22+
const { datasetNameOrId } = this.args;
23+
24+
const client = await getLoggedClientOrThrow();
25+
26+
const existingDataset = await tryToGetDataset(client, datasetNameOrId);
27+
28+
if (!existingDataset) {
29+
error({
30+
message: `Dataset with ID or name "${datasetNameOrId}" not found.`,
31+
});
32+
33+
return;
34+
}
35+
36+
const confirmed = await confirmAction({ type: 'Dataset' });
37+
38+
if (!confirmed) {
39+
info({ message: 'Dataset deletion has been aborted.' });
40+
return;
41+
}
42+
43+
const { id, name } = existingDataset.dataset;
44+
45+
try {
46+
await existingDataset.datasetClient.delete();
47+
48+
success({
49+
message: `Dataset with ID ${chalk.yellow(id)}${name ? ` (called ${chalk.yellow(name)})` : ''} has been deleted.`,
50+
stdout: true,
51+
});
52+
} catch (err) {
53+
const casted = err as ApifyApiError;
54+
55+
error({
56+
message: `Failed to delete Dataset with ID ${chalk.yellow(id)}\n ${casted.message || casted}`,
57+
});
58+
}
59+
}
60+
}

0 commit comments

Comments
 (0)