diff --git a/.changeset/fuzzy-store-variables.md b/.changeset/fuzzy-store-variables.md new file mode 100644 index 00000000000..ee89cd880fc --- /dev/null +++ b/.changeset/fuzzy-store-variables.md @@ -0,0 +1,5 @@ +--- +'@shopify/cli': patch +--- + +Reject invalid variable values before store execute requests are sent. diff --git a/packages/store/src/cli/services/store/execute/request.test.ts b/packages/store/src/cli/services/store/execute/request.test.ts index e0bebbfc557..9fc0672e8ec 100644 --- a/packages/store/src/cli/services/store/execute/request.test.ts +++ b/packages/store/src/cli/services/store/execute/request.test.ts @@ -110,6 +110,24 @@ describe('prepareStoreExecuteRequest', () => { expect(request.parsedOperation.operationDefinition.operation).toBe('mutation') }) + test.each(['null', '[]', '"value"', 'true', '42'])('throws when inline variables are %s', async (variables) => { + await expect( + prepareStoreExecuteRequest({ + query: 'query { shop { name } }', + variables, + }), + ).rejects.toThrow('expected a non-null JSON object') + }) + + test('throws when inline variables are empty', async () => { + await expect( + prepareStoreExecuteRequest({ + query: 'query { shop { name } }', + variables: ' ', + }), + ).rejects.toThrow('--variables') + }) + test('throws when variables contain invalid JSON', async () => { await expect( prepareStoreExecuteRequest({ @@ -119,6 +137,29 @@ describe('prepareStoreExecuteRequest', () => { ).rejects.toThrow('Invalid JSON') }) + test.each(['null', '[]', '"value"', 'true', '42'])('throws when file variables are %s', async (variables) => { + await inTemporaryDirectory(async (tmpDir) => { + const variableFile = joinPath(tmpDir, 'variables.json') + await writeFile(variableFile, variables) + + await expect( + prepareStoreExecuteRequest({ + query: 'query { shop { name } }', + variableFile, + }), + ).rejects.toThrow('expected a non-null JSON object') + }) + }) + + test('accepts an empty variable object', async () => { + await expect( + prepareStoreExecuteRequest({ + query: 'query { shop { name } }', + variables: '{}', + }), + ).resolves.toMatchObject({parsedVariables: {}}) + }) + test('reads variables from a file', async () => { await inTemporaryDirectory(async (tmpDir) => { // Given diff --git a/packages/store/src/cli/services/store/execute/request.ts b/packages/store/src/cli/services/store/execute/request.ts index 71289bdaba3..618c1fa370e 100644 --- a/packages/store/src/cli/services/store/execute/request.ts +++ b/packages/store/src/cli/services/store/execute/request.ts @@ -47,14 +47,36 @@ async function readQuery(input: {query?: string; queryFile?: string}): Promise | ReturnType, +): {[key: string]: unknown} { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new AbortError( + outputContent`Invalid variables in ${source}: expected a non-null JSON object.`, + 'Please provide a JSON object for variables.', + ) + } + + return value as {[key: string]: unknown} +} + async function parseVariables( variables?: string, variableFile?: string, ): Promise<{[key: string]: unknown} | undefined> { - if (variables) { + if (variables !== undefined) { + if (!variables.trim()) { + throw new AbortError( + outputContent`The ${outputToken.yellow('--variables')} flag value is empty.`, + 'Please provide a non-null JSON object for variables.', + ) + } + try { - return JSON.parse(variables) + return validateVariables(JSON.parse(variables), outputToken.yellow('--variables')) } catch (error) { + if (error instanceof AbortError) throw error const errorMessage = error instanceof Error ? error.message : 'Unknown error' throw new AbortError( outputContent`Invalid JSON in ${outputToken.yellow('--variables')} flag: ${errorMessage}`, @@ -72,8 +94,9 @@ async function parseVariables( const fileContent = await readFile(variableFile, {encoding: 'utf8'}) try { - return JSON.parse(fileContent) + return validateVariables(JSON.parse(fileContent), outputToken.path(variableFile)) } catch (error) { + if (error instanceof AbortError) throw error const errorMessage = error instanceof Error ? error.message : 'Unknown error' throw new AbortError( outputContent`Invalid JSON in variable file ${outputToken.path(variableFile)}: ${errorMessage}`,