Skip to content
Open
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
6 changes: 4 additions & 2 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,7 @@ FLAGS
DESCRIPTION
Executes Actor remotely using your authenticated account.
Reads input from local key-value store by default.
Inspect the input schema first with "apify actors info <actor> --input".

USAGE
$ apify actors call [actorId] [-b <value>]
Expand All @@ -797,8 +798,9 @@ ARGUMENTS
FLAGS
-b, --build=<value> Tag or number of the build to
run (e.g. "latest" or "1.2.34").
-i, --input=<value> Optional JSON input to be
given to the Actor.
-i, --input=<value> Optional inline JSON object
input for the Actor. Wrap the JSON in quotes to avoid
shell parsing issues. For JSON files, use --input-file.
-f, --input-file=<value> Optional path to a file with
JSON input to be given to the Actor. The file must be a
valid JSON file. You can also specify `-` to read from
Expand Down
19 changes: 15 additions & 4 deletions src/commands/actors/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export class ActorsCallCommand extends ApifyCommand<typeof ActorsCallCommand> {

static override description =
'Executes Actor remotely using your authenticated account.\n' +
'Reads input from local key-value store by default.';
'Reads input from local key-value store by default.\n' +
'To inspect the input schema before creating a JSON input, use "apify actors info <actor> --input".';

static override group = 'Apify Console';

Expand All @@ -43,6 +44,10 @@ export class ActorsCallCommand extends ApifyCommand<typeof ActorsCallCommand> {
description: 'Call a specific Actor by its full name.',
command: 'apify call apify/hello-world',
},
{
description: 'Inspect the Actor input schema before preparing JSON input.',
command: 'apify actors info apify/hello-world --input',
},
{
description: 'Call an Actor with inline JSON input.',
command: `apify call apify/hello-world --input '{"url":"https://example.com"}'`,
Expand All @@ -59,7 +64,8 @@ export class ActorsCallCommand extends ApifyCommand<typeof ActorsCallCommand> {
...SharedRunOnCloudFlags('Actor'),
input: Flags.string({
char: 'i',
description: 'Optional JSON input to be given to the Actor.',
description:
'Optional inline JSON object input for the Actor. To avoid shell parsing issues, wrap the JSON in quotes. For JSON files, use --input-file.',
required: false,
stdin: StdinMode.Stringified,
exclusive: ['input-file'],
Expand Down Expand Up @@ -103,7 +109,10 @@ export class ActorsCallCommand extends ApifyCommand<typeof ActorsCallCommand> {
const usernameOrId = userInfo.username || (userInfo.id as string);

if (this.flags.json && this.flags.outputDataset) {
error({ message: 'You cannot use both the --json and --output-dataset flags when running this command.' });
error({
message:
'You cannot use both --json and --output-dataset. Use --json for run details or --output-dataset for dataset items.',
});
process.exitCode = CommandExitCodes.InvalidInput;

return;
Expand Down Expand Up @@ -136,7 +145,9 @@ export class ActorsCallCommand extends ApifyCommand<typeof ActorsCallCommand> {
runOpts.memory = this.flags.memory;
}

const inputOverride = await getInputOverride(cwd, this.flags.input, this.flags.inputFile);
const inputOverride = await getInputOverride(cwd, this.flags.input, this.flags.inputFile, {
schemaHint: `Run "apify actors info ${userFriendlyId} --input" to inspect the Actor input schema.`,
});

// Means we couldn't resolve input, so we should exit
if (inputOverride === false) {
Expand Down
61 changes: 52 additions & 9 deletions src/lib/commands/resolve-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ import { CommandExitCodes } from '../consts.js';
import { error } from '../outputs.js';
import { getLocalInput } from '../utils.js';

interface InputOverrideOptions {
schemaHint?: string;
}

function withSchemaHint(message: string, schemaHint?: string) {
return schemaHint ? `${message}\n${schemaHint}` : message;
}

export function resolveInput(cwd: string, inputOverride: Record<string, unknown> | undefined) {
let inputToUse: Record<string, unknown> | undefined;
let contentType!: string;
Expand Down Expand Up @@ -39,9 +47,15 @@ export function resolveInput(cwd: string, inputOverride: Record<string, unknown>
return { inputToUse, contentType };
}

export async function getInputOverride(cwd: string, inputFlag: string | undefined, inputFileFlag: string | undefined) {
export async function getInputOverride(
cwd: string,
inputFlag: string | undefined,
inputFileFlag: string | undefined,
options: InputOverrideOptions = {},
) {
let input: Record<string, unknown> | undefined;
let source: 'stdin' | 'input' | string;
const { schemaHint } = options;

if (!inputFlag && !inputFileFlag) {
// Try reading stdin
Expand All @@ -52,15 +66,22 @@ export async function getInputOverride(cwd: string, inputFlag: string | undefine
const parsed = JSON.parse(stdin.toString('utf8'));

if (Array.isArray(parsed)) {
error({ message: 'The provided input is invalid. It should be an object, not an array.' });
error({
message: withSchemaHint('The provided input is invalid. It should be an object, not an array.', schemaHint),
});
process.exitCode = CommandExitCodes.InvalidInput;
return false;
}

input = parsed;
source = 'stdin';
} catch (err) {
error({ message: `Cannot parse JSON input from standard input.\n ${(err as Error).message}` });
error({
message: withSchemaHint(
`Cannot parse JSON input from standard input.\n ${(err as Error).message}`,
schemaHint,
),
});
process.exitCode = CommandExitCodes.InvalidInput;
return false;
}
Expand Down Expand Up @@ -98,7 +119,10 @@ export async function getInputOverride(cwd: string, inputFlag: string | undefine

if (fileExists || inputLooksLikePath) {
error({
message: `Providing a JSON file path in the --input flag is not supported. Use the "--input-file=" flag instead`,
message: withSchemaHint(
`Providing a JSON file path in the --input flag is not supported. Use the "--input-file=" flag instead`,
schemaHint,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: maybe in this case the schema hint does not really fit (user used a wrong flag, not invalid input json).
Not much of an issue tho, so feel free to ignore.

),
});
process.exitCode = CommandExitCodes.InvalidInput;
return false;
Expand All @@ -108,15 +132,22 @@ export async function getInputOverride(cwd: string, inputFlag: string | undefine
const parsed = JSON.parse(inputFlag);

if (Array.isArray(parsed)) {
error({ message: 'The provided input is invalid. It should be an object, not an array.' });
error({
message: withSchemaHint(
'The provided input is invalid. It should be an object, not an array.',
schemaHint,
),
});
process.exitCode = CommandExitCodes.InvalidInput;
return false;
}

input = parsed;
source = 'input';
} catch (err) {
error({ message: `Cannot parse JSON input.\n ${(err as Error).message}` });
error({
message: withSchemaHint(`Cannot parse JSON input.\n ${(err as Error).message}`, schemaHint),
});
process.exitCode = CommandExitCodes.InvalidInput;
return false;
}
Expand All @@ -143,7 +174,12 @@ export async function getInputOverride(cwd: string, inputFlag: string | undefine
const parsed = JSON.parse(fileContent);

if (Array.isArray(parsed)) {
error({ message: 'The provided input is invalid. It should be an object, not an array.' });
error({
message: withSchemaHint(
'The provided input is invalid. It should be an object, not an array.',
schemaHint,
),
});
process.exitCode = CommandExitCodes.InvalidInput;
return false;
}
Expand All @@ -159,16 +195,23 @@ export async function getInputOverride(cwd: string, inputFlag: string | undefine
const parsed = JSON.parse(inputFileFlag);

if (Array.isArray(parsed)) {
error({ message: 'The provided input is invalid. It should be an object, not an array.' });
error({
message: withSchemaHint(
'The provided input is invalid. It should be an object, not an array.',
schemaHint,
),
});
process.exitCode = CommandExitCodes.InvalidInput;
return false;
}

input = parsed;
source = inputFileFlag;
} catch {
const message = `Cannot read input file at path "${fullPath}".\n ${(fsError as Error).message}`;

error({
message: `Cannot read input file at path "${fullPath}".\n ${(fsError as Error).message}`,
message: withSchemaHint(message, fsError instanceof SyntaxError ? schemaHint : undefined),
});
process.exitCode = CommandExitCodes.InvalidInput;
return false;
Expand Down
55 changes: 55 additions & 0 deletions test/local/lib/resolve-input.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import process from 'node:process';

import { afterEach, describe, expect, it } from 'vitest';

import { getInputOverride } from '../../../src/lib/commands/resolve-input.js';
import { CommandExitCodes } from '../../../src/lib/consts.js';
import { useConsoleSpy } from '../../__setup__/hooks/useConsoleSpy.js';

const SCHEMA_HINT = 'Run "apify actors info apify/hello-world --input" to inspect the Actor input schema.';

const { logMessages } = useConsoleSpy();

describe('getInputOverride', () => {
afterEach(() => {
process.exitCode = undefined;
});

it('appends schema hint to --input file path errors when provided', async () => {
const result = await getInputOverride(process.cwd(), './input.json', undefined, { schemaHint: SCHEMA_HINT });

expect(result).toBe(false);
expect(process.exitCode).toBe(CommandExitCodes.InvalidInput);
const stderr = logMessages.error.join('\n');
expect(stderr).toContain('Use the "--input-file=" flag instead');
expect(stderr).toContain(SCHEMA_HINT);
});

it('keeps --input file path errors unchanged when schema hint is omitted', async () => {
const result = await getInputOverride(process.cwd(), './input.json', undefined);

expect(result).toBe(false);
expect(process.exitCode).toBe(CommandExitCodes.InvalidInput);
expect(logMessages.error.join('\n')).not.toContain('apify actors info');
});

it('appends schema hint to malformed inline JSON errors', async () => {
const result = await getInputOverride(process.cwd(), '{"url":', undefined, { schemaHint: SCHEMA_HINT });

expect(result).toBe(false);
expect(process.exitCode).toBe(CommandExitCodes.InvalidInput);
const stderr = logMessages.error.join('\n');
expect(stderr).toContain('Cannot parse JSON input.');
expect(stderr).toContain(SCHEMA_HINT);
});

it('appends schema hint to array input errors', async () => {
const result = await getInputOverride(process.cwd(), '[]', undefined, { schemaHint: SCHEMA_HINT });

expect(result).toBe(false);
expect(process.exitCode).toBe(CommandExitCodes.InvalidInput);
const stderr = logMessages.error.join('\n');
expect(stderr).toContain('It should be an object, not an array.');
expect(stderr).toContain(SCHEMA_HINT);
});
});
Loading