From 4061a8aefa7057b6c3ab41bb8a622580047223ab Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Mon, 31 Aug 2026 13:21:49 +0200 Subject: [PATCH] Add typed JSON output to store inspection --- packages/cli/README.md | 55 +++++++++++++++++++ packages/cli/oclif.manifest.json | 4 +- .../store/src/cli/commands/store/info.test.ts | 9 ++- packages/store/src/cli/commands/store/info.ts | 5 ++ .../store/src/cli/commands/store/list.test.ts | 17 ++++-- packages/store/src/cli/commands/store/list.ts | 9 ++- .../src/cli/services/store/info/index.test.ts | 2 + .../src/cli/services/store/info/result.ts | 4 +- .../src/cli/services/store/info/types.ts | 55 +++++++++++-------- .../store/src/cli/services/store/list.test.ts | 6 +- packages/store/src/cli/services/store/list.ts | 8 +-- .../cli/services/store/list/result.test.ts | 38 +++++-------- .../src/cli/services/store/list/result.ts | 25 ++------- .../src/cli/services/store/list/types.ts | 53 +++++++++++------- 14 files changed, 180 insertions(+), 110 deletions(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index 72e3701084c..016e9598836 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -4204,6 +4204,33 @@ DESCRIPTION Use `--json` for machine-readable output. + Output from `--json` conforms to the `StoreInfoResult` schema. + + Use `--json-schema` to print the schema directly: + + ```ts + interface StoreInfoResult { + id?: string + displayName?: string + subdomain: string + organizationId?: string + organizationName?: string + storeOwner?: StoreInfoStoreOwner + type?: string + plan?: string + featurePreview?: string + adminUrl?: string + accessUrl?: string + saveUrl?: string + authScopes?: string[] + } + + interface StoreInfoStoreOwner { + name?: string + email?: string + } + ``` + EXAMPLES $ shopify store info --store shop.myshopify.com @@ -4250,6 +4277,34 @@ DESCRIPTION Run `shopify organization list` to find organization IDs. + Output from `--json` conforms to the `StoreListResult` schema. + + Use `--json-schema` to print the schema directly: + + ```ts + interface StoreListResult { + stores: StoreListEntry[] + organization?: StoreListOrganization + notice?: string + truncated?: boolean + } + + interface StoreListEntry { + id?: string + store: string + createdAt: string + organizationId: string + organizationName: string + name?: string + type?: string + } + + interface StoreListOrganization { + id: string + name: string + } + ``` + EXAMPLES $ shopify store list diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 261e43360d8..aae3e2b9a94 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -7803,7 +7803,7 @@ "args": { }, "customPluginName": "@shopify/store", - "description": "Returns available metadata about a store you have access to, such as its id, display name, subdomain, organization, store owner, type, plan, feature preview, admin URL, and access and save URLs for preview stores.\n\nSome details may be omitted when they are not available for the store.\n\nUse `--json` for machine-readable output.", + "description": "Returns available metadata about a store you have access to, such as its id, display name, subdomain, organization, store owner, type, plan, feature preview, admin URL, and access and save URLs for preview stores.\n\nSome details may be omitted when they are not available for the store.\n\nUse `--json` for machine-readable output.\n\nOutput from `--json` conforms to the `StoreInfoResult` schema.\n\nUse `--json-schema` to print the schema directly:\n\n```ts\ninterface StoreInfoResult {\n id?: string\n displayName?: string\n subdomain: string\n organizationId?: string\n organizationName?: string\n storeOwner?: StoreInfoStoreOwner\n type?: string\n plan?: string\n featurePreview?: string\n adminUrl?: string\n accessUrl?: string\n saveUrl?: string\n authScopes?: string[]\n}\n\ninterface StoreInfoStoreOwner {\n name?: string\n email?: string\n}\n```", "descriptionWithMarkdown": "Returns available metadata about a store you have access to, such as its id, display name, subdomain, organization, store owner, type, plan, feature preview, admin URL, and access and save URLs for preview stores.\n\nSome details may be omitted when they are not available for the store.\n\nUse `--json` for machine-readable output.", "examples": [ "<%= config.bin %> <%= command.id %> --store shop.myshopify.com", @@ -7869,7 +7869,7 @@ "args": { }, "customPluginName": "@shopify/store", - "description": "Lists stores in a Shopify organization available to the current CLI account.\n\nWhen more than one organization is available, the command prompts you to pick one unless you provide `--organization-id`. In that case, `--organization-id` is required in non-interactive environments.\n\nRun `<%= config.bin %> organization list` to find organization IDs.", + "description": "Lists stores in a Shopify organization available to the current CLI account.\n\nWhen more than one organization is available, the command prompts you to pick one unless you provide `--organization-id`. In that case, `--organization-id` is required in non-interactive environments.\n\nRun `<%= config.bin %> organization list` to find organization IDs.\n\nOutput from `--json` conforms to the `StoreListResult` schema.\n\nUse `--json-schema` to print the schema directly:\n\n```ts\ninterface StoreListResult {\n stores: StoreListEntry[]\n organization?: StoreListOrganization\n notice?: string\n truncated?: boolean\n}\n\ninterface StoreListEntry {\n id?: string\n store: string\n createdAt: string\n organizationId: string\n organizationName: string\n name?: string\n type?: string\n}\n\ninterface StoreListOrganization {\n id: string\n name: string\n}\n```", "descriptionWithMarkdown": "Lists stores in a Shopify organization available to the current CLI account.\n\nWhen more than one organization is available, the command prompts you to pick one unless you provide `--organization-id`. In that case, `--organization-id` is required in non-interactive environments.\n\nRun `<%= config.bin %> organization list` to find organization IDs.", "examples": [ "<%= config.bin %> <%= command.id %>", diff --git a/packages/store/src/cli/commands/store/info.test.ts b/packages/store/src/cli/commands/store/info.test.ts index f1f43169aea..f89d0a5044d 100644 --- a/packages/store/src/cli/commands/store/info.test.ts +++ b/packages/store/src/cli/commands/store/info.test.ts @@ -1,6 +1,7 @@ import StoreInfo from './info.js' import {getStoreInfo} from '../../services/store/info/index.js' import {renderStoreInfoResult} from '../../services/store/info/result.js' +import {storeInfoJsonOutputSchema} from '../../services/store/info/types.js' import {beforeEach, describe, expect, test, vi} from 'vitest' vi.mock('../../services/store/info/index.js') @@ -18,9 +19,7 @@ describe('store info command', () => { test('passes the store flag through to the service', async () => { await StoreInfo.run(['--store', 'shop.myshopify.com']) - expect(getStoreInfo).toHaveBeenCalledWith({ - store: 'shop.myshopify.com', - }) + expect(getStoreInfo).toHaveBeenCalledWith({store: 'shop.myshopify.com'}) expect(renderStoreInfoResult).toHaveBeenCalledWith( expect.objectContaining({subdomain: 'shop.myshopify.com'}), 'text', @@ -37,4 +36,8 @@ describe('store info command', () => { expect(StoreInfo.flags.store).toBeDefined() expect(StoreInfo.flags.json).toBeDefined() }) + + test('exposes the JSON output schema', () => { + expect(StoreInfo.jsonOutputSchema).toBe(storeInfoJsonOutputSchema) + }) }) diff --git a/packages/store/src/cli/commands/store/info.ts b/packages/store/src/cli/commands/store/info.ts index e1aa5a93c1f..3e03162fdc5 100644 --- a/packages/store/src/cli/commands/store/info.ts +++ b/packages/store/src/cli/commands/store/info.ts @@ -1,5 +1,6 @@ import {getStoreInfo} from '../../services/store/info/index.js' import {renderStoreInfoResult} from '../../services/store/info/result.js' +import {storeInfoJsonOutputSchema} from '../../services/store/info/types.js' import StoreCommand from '../../utilities/store-command.js' import {storeFlags} from '../../flags.js' import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' @@ -26,6 +27,10 @@ Use \`--json\` for machine-readable output.` store: storeFlags.store, } + static get jsonOutputSchema() { + return storeInfoJsonOutputSchema + } + public async run(): Promise { const {flags} = await this.parse(StoreInfo) diff --git a/packages/store/src/cli/commands/store/list.test.ts b/packages/store/src/cli/commands/store/list.test.ts index 0e8933bc21d..29a7030dd40 100644 --- a/packages/store/src/cli/commands/store/list.test.ts +++ b/packages/store/src/cli/commands/store/list.test.ts @@ -1,6 +1,7 @@ import StoreList from './list.js' import {listStores} from '../../services/store/list.js' -import {writeStoreListResult} from '../../services/store/list/result.js' +import {presentStoreListResult} from '../../services/store/list/result.js' +import {storeListJsonOutputSchema} from '../../services/store/list/types.js' import {describe, expect, test, vi} from 'vitest' vi.mock('../../services/store/list.js') @@ -9,16 +10,16 @@ vi.mock('../../services/store/attribution.js') describe('store list command', () => { test('runs the list service and writes text output by default', async () => { - vi.mocked(listStores).mockResolvedValue({stores: [], source: 'organization'}) + vi.mocked(listStores).mockResolvedValue({stores: []}) await StoreList.run([]) expect(listStores).toHaveBeenCalledWith({organizationId: undefined}) - expect(writeStoreListResult).toHaveBeenCalledWith({stores: [], source: 'organization'}, 'text') + expect(presentStoreListResult).toHaveBeenCalledWith({stores: []}, 'text') }) test('passes the organization id through to the list service', async () => { - vi.mocked(listStores).mockResolvedValue({stores: [], source: 'organization'}) + vi.mocked(listStores).mockResolvedValue({stores: []}) await StoreList.run(['--organization-id', '1234567']) @@ -26,12 +27,12 @@ describe('store list command', () => { }) test('writes json output when requested', async () => { - vi.mocked(listStores).mockResolvedValue({stores: [], source: 'organization'}) + vi.mocked(listStores).mockResolvedValue({stores: []}) await StoreList.run(['--json']) expect(listStores).toHaveBeenCalledWith({organizationId: undefined}) - expect(writeStoreListResult).toHaveBeenCalledWith({stores: [], source: 'organization'}, 'json') + expect(presentStoreListResult).toHaveBeenCalledWith({stores: []}, 'json') }) test('defines the expected flags', () => { @@ -39,4 +40,8 @@ describe('store list command', () => { expect(StoreList.flags['organization-id']).toBeDefined() expect(StoreList.flags).not.toHaveProperty('from') }) + + test('exposes the JSON output schema', () => { + expect(StoreList.jsonOutputSchema).toBe(storeListJsonOutputSchema) + }) }) diff --git a/packages/store/src/cli/commands/store/list.ts b/packages/store/src/cli/commands/store/list.ts index 3222d3bffb2..150f424bc32 100644 --- a/packages/store/src/cli/commands/store/list.ts +++ b/packages/store/src/cli/commands/store/list.ts @@ -1,5 +1,6 @@ import {listStores} from '../../services/store/list.js' -import {writeStoreListResult} from '../../services/store/list/result.js' +import {presentStoreListResult} from '../../services/store/list/result.js' +import {storeListJsonOutputSchema} from '../../services/store/list/types.js' import {storeFlags} from '../../flags.js' import StoreCommand from '../../utilities/store-command.js' import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' @@ -31,10 +32,14 @@ Run \`<%= config.bin %> organization list\` to find organization IDs.` }), } + static get jsonOutputSchema() { + return storeListJsonOutputSchema + } + public async run(): Promise { const {flags} = await this.parse(StoreList) const result = await listStores({organizationId: flags['organization-id']}) - writeStoreListResult(result, flags.json ? 'json' : 'text') + presentStoreListResult(result, flags.json ? 'json' : 'text') } } diff --git a/packages/store/src/cli/services/store/info/index.test.ts b/packages/store/src/cli/services/store/info/index.test.ts index 6cebd2adae8..cd57d2d3b46 100644 --- a/packages/store/src/cli/services/store/info/index.test.ts +++ b/packages/store/src/cli/services/store/info/index.test.ts @@ -1,4 +1,5 @@ import {getStoreInfo} from './index.js' +import {storeInfoJsonOutputSchema} from './types.js' import {StoreLookupStoreNotFoundError, fetchDestinationsContext} from '../../../utilities/store-lookup/destinations.js' import {fetchOrganizationShop} from '../../../utilities/store-lookup/organization-shop.js' import {STORE_AUTH_APP_CLIENT_ID} from '../auth/config.js' @@ -149,6 +150,7 @@ describe('getStoreInfo', () => { featurePreview: 'extended_variants', adminUrl: 'https://admin.shopify.com/store/shop', }) + expect(storeInfoJsonOutputSchema.validate(result)).toEqual(result) }) test('returns fresh access and save URLs for locally stored preview stores', async () => { diff --git a/packages/store/src/cli/services/store/info/result.ts b/packages/store/src/cli/services/store/info/result.ts index a4c009aab41..6af169ed4fe 100644 --- a/packages/store/src/cli/services/store/info/result.ts +++ b/packages/store/src/cli/services/store/info/result.ts @@ -1,13 +1,13 @@ +import {storeInfoJsonOutputSchema, type StoreInfoResult, type StoreInfoStoreOwner} from './types.js' import {outputResult} from '@shopify/cli-kit/node/output' import {renderInfo, type InlineToken, type LinkToken} from '@shopify/cli-kit/node/ui' import {capitalizeWords} from '@shopify/cli-kit/common/string' -import type {StoreInfoResult, StoreInfoStoreOwner} from './types.js' type StoreInfoOutputFormat = 'text' | 'json' export function renderStoreInfoResult(result: StoreInfoResult, format: StoreInfoOutputFormat): void { if (format === 'json') { - outputResult(JSON.stringify(result, null, 2)) + outputResult(storeInfoJsonOutputSchema.encode(result)) return } const actions = storeActions(result) diff --git a/packages/store/src/cli/services/store/info/types.ts b/packages/store/src/cli/services/store/info/types.ts index ef037f371f2..57ddf51fab1 100644 --- a/packages/store/src/cli/services/store/info/types.ts +++ b/packages/store/src/cli/services/store/info/types.ts @@ -1,24 +1,33 @@ -export interface StoreInfoStoreOwner { - name?: string - email?: string -} +import {defineJsonOutputSchema, type InferJsonOutputSchema} from '@shopify/cli-kit/node/json-output-schema' +import {zod} from '@shopify/cli-kit/node/schema' -export interface StoreInfoResult { - id?: string - displayName?: string - subdomain: string - organizationId?: string - organizationName?: string - storeOwner?: StoreInfoStoreOwner - type?: string - // Admin API public display name for store-auth stores, or public plan handle for BP-backed stores. - plan?: string - featurePreview?: string - adminUrl?: string - accessUrl?: string - saveUrl?: string - // Preapproved Admin API access scopes for the store (currently only preview stores, which - // cache the scopes granted at creation time). Preview stores aren't a logged-in experience, so - // there's no way to grant additional scopes later. - authScopes?: string[] -} +const StoreInfoStoreOwnerSchema = zod.object({ + name: zod.string().optional(), + email: zod.string().optional(), +}) + +export const storeInfoJsonOutputSchema = defineJsonOutputSchema({ + name: 'StoreInfoResult', + schema: zod.object({ + id: zod.string().optional(), + displayName: zod.string().optional(), + subdomain: zod.string(), + organizationId: zod.string().optional(), + organizationName: zod.string().optional(), + storeOwner: StoreInfoStoreOwnerSchema.optional(), + type: zod.string().optional(), + // Admin API public display name for store-auth stores, or public plan handle for BP-backed stores. + plan: zod.string().optional(), + featurePreview: zod.string().optional(), + adminUrl: zod.string().optional(), + accessUrl: zod.string().optional(), + saveUrl: zod.string().optional(), + // Preapproved Admin API access scopes for preview stores. Preview stores aren't a logged-in + // experience, so there's no way to grant additional scopes later. + authScopes: zod.array(zod.string()).optional(), + }), + definitions: {StoreInfoStoreOwner: StoreInfoStoreOwnerSchema}, +}) + +export type StoreInfoStoreOwner = zod.infer +export type StoreInfoResult = InferJsonOutputSchema diff --git a/packages/store/src/cli/services/store/list.test.ts b/packages/store/src/cli/services/store/list.test.ts index c309fe9cf70..59b216e66c7 100644 --- a/packages/store/src/cli/services/store/list.test.ts +++ b/packages/store/src/cli/services/store/list.test.ts @@ -1,5 +1,6 @@ import {listStores} from './list.js' import * as bpSource from './list/bp-source.js' +import {storeListJsonOutputSchema} from './list/types.js' import {describe, expect, test, vi} from 'vitest' import {ensureAuthenticatedBusinessPlatform} from '@shopify/cli-kit/node/session' import {AbortError} from '@shopify/cli-kit/node/error' @@ -42,9 +43,9 @@ describe('listStores', () => { expect(renderAutocompletePrompt).not.toHaveBeenCalled() expect(result).toEqual({ stores: [orgEntry], - source: 'organization', organization: {id: '1234', name: 'Acme'}, }) + expect(storeListJsonOutputSchema.validate(result)).toEqual(result) }) test('uses the requested organization id when provided', async () => { @@ -107,7 +108,6 @@ describe('listStores', () => { expect(result).toEqual({ stores: [], - source: 'organization', notice: "Couldn't resolve a Shopify account for the current CLI session.", }) }) @@ -117,7 +117,7 @@ describe('listStores', () => { const result = await listStores() - expect(result).toEqual({stores: [], source: 'organization'}) + expect(result).toEqual({stores: []}) }) test('propagates store listing failures', async () => { diff --git a/packages/store/src/cli/services/store/list.ts b/packages/store/src/cli/services/store/list.ts index 15658f3ba6f..f497b83af04 100644 --- a/packages/store/src/cli/services/store/list.ts +++ b/packages/store/src/cli/services/store/list.ts @@ -1,6 +1,6 @@ import {listBusinessPlatformStores} from './list/bp-source.js' import {STORE_LIST_LIMIT} from './list/constants.js' -import {type ListStoresResult, type StoreListEntry, type StoreListOrganization} from './list/types.js' +import {type StoreListEntry, type StoreListOrganization, type StoreListResult} from './list/types.js' import {AbortError} from '@shopify/cli-kit/node/error' import {ensureAuthenticatedBusinessPlatform} from '@shopify/cli-kit/node/session' import {isTTY, renderAutocompletePrompt} from '@shopify/cli-kit/node/ui' @@ -10,20 +10,19 @@ interface ListStoresOptions { organizationId?: number } -export async function listStores(options: ListStoresOptions = {}): Promise { +export async function listStores(options: ListStoresOptions = {}): Promise { const token = await ensureAuthenticatedBusinessPlatform() const organizationsResult = await fetchOrganizationsWithAccessInfo(token) if (!organizationsResult.currentUserResolved) { return { stores: [], - source: 'organization', notice: "Couldn't resolve a Shopify account for the current CLI session.", } } if (organizationsResult.organizations.length === 0) { - return {stores: [], source: 'organization'} + return {stores: []} } if (!options.organizationId && organizationsResult.organizations.length > 1 && !isTTY()) { @@ -43,7 +42,6 @@ export async function listStores(options: ListStoresOptions = {}): Promise { +describe('presentStoreListResult', () => { beforeEach(() => { mockAndCaptureOutput().clear() }) @@ -12,9 +12,8 @@ describe('writeStoreListResult', () => { test('renders organization context and rows with subdomain, name, type, and created date', () => { const output = mockAndCaptureOutput() - writeStoreListResult( + presentStoreListResult( { - source: 'organization', organization, stores: [ { @@ -44,9 +43,8 @@ describe('writeStoreListResult', () => { test('renders the subdomain handle for non-myshopify hosts (local dev)', () => { const output = mockAndCaptureOutput() - writeStoreListResult( + presentStoreListResult( { - source: 'organization', organization, stores: [ { @@ -65,12 +63,11 @@ describe('writeStoreListResult', () => { expect(output.info()).not.toContain('my-shop.my.shop.dev') }) - test('writes the unresolved-session notice to stderr and the empty state to stdout', () => { + test('warns about the unresolved session and renders the empty state', () => { const output = mockAndCaptureOutput() - writeStoreListResult( + presentStoreListResult( { - source: 'organization', stores: [], notice: "Couldn't resolve a Shopify account for the current CLI session.", }, @@ -85,7 +82,7 @@ describe('writeStoreListResult', () => { test('renders the selected organization empty state', () => { const output = mockAndCaptureOutput() - writeStoreListResult({source: 'organization', organization, stores: []}, 'text') + presentStoreListResult({organization, stores: []}, 'text') expect(output.info()).toContain('No stores found in Acme.') }) @@ -93,7 +90,7 @@ describe('writeStoreListResult', () => { test('renders the fallback organization empty state when no organization is selected', () => { const output = mockAndCaptureOutput() - writeStoreListResult({source: 'organization', stores: []}, 'text') + presentStoreListResult({stores: []}, 'text') expect(output.info()).toContain('No stores found in your Shopify organization.') expect(output.info()).toContain('shopify store auth list') @@ -102,9 +99,8 @@ describe('writeStoreListResult', () => { test('emits a {stores, organization} JSON document on stdout', () => { const output = mockAndCaptureOutput() - writeStoreListResult( + presentStoreListResult( { - source: 'organization', organization, stores: [ { @@ -140,9 +136,8 @@ describe('writeStoreListResult', () => { test('includes unresolved-session notices in JSON output', () => { const output = mockAndCaptureOutput() - writeStoreListResult( + presentStoreListResult( { - source: 'organization', stores: [], notice: "Couldn't resolve a Shopify account for the current CLI session.", }, @@ -153,12 +148,10 @@ describe('writeStoreListResult', () => { stores: [], notice: "Couldn't resolve a Shopify account for the current CLI session.", }) - expect(output.warn()).toContain("Couldn't resolve a Shopify account for the current CLI session.") }) - test('warns on stderr when the listing was truncated, in both text and json', () => { + test('includes the structured truncation flag in JSON output', () => { const result = { - source: 'organization' as const, organization, stores: [ { @@ -171,14 +164,9 @@ describe('writeStoreListResult', () => { truncated: true, } - const textOutput = mockAndCaptureOutput() - writeStoreListResult(result, 'text') - expect(textOutput.warn()).toContain('Showing the 250 most recent stores in Acme. More stores exist') - const jsonOutput = mockAndCaptureOutput() - writeStoreListResult(result, 'json') - expect(jsonOutput.warn()).toContain('Showing the 250 most recent stores in Acme. More stores exist') - // The structured truncation flag is part of the JSON document on stdout (prose stays on stderr). + presentStoreListResult(result, 'json') expect(jsonOutput.output()).toContain('"truncated": true') + expect(jsonOutput.warn()).toContain('Showing the 250 most recent stores in Acme. More stores exist') }) }) diff --git a/packages/store/src/cli/services/store/list/result.ts b/packages/store/src/cli/services/store/list/result.ts index 925dbba7fee..bc189d9a3d7 100644 --- a/packages/store/src/cli/services/store/list/result.ts +++ b/packages/store/src/cli/services/store/list/result.ts @@ -1,41 +1,28 @@ import {STORE_LIST_LIMIT} from './constants.js' -import {type ListStoresResult, type StoreListEntry} from './types.js' +import {storeListJsonOutputSchema, type StoreListEntry, type StoreListResult} from './types.js' import {extractSubdomain, formatShortDate} from '../display.js' import {storeTypeLabel} from '../store-type.js' import {outputInfo, outputResult, outputWarn} from '@shopify/cli-kit/node/output' import {renderTable} from '@shopify/cli-kit/node/ui' -export function writeStoreListResult(result: ListStoresResult, format: 'text' | 'json'): void { - // Human diagnostics always go to stderr so they never corrupt the JSON document on stdout, and so - // the truncation signal is visible in both formats. +export function presentStoreListResult(result: StoreListResult, format: 'text' | 'json'): void { if (result.notice) outputWarn(result.notice) if (result.truncated) outputWarn(truncationWarning(result)) if (format === 'json') { - outputResult( - JSON.stringify( - { - stores: result.stores, - ...(result.organization ? {organization: result.organization} : {}), - ...(result.notice ? {notice: result.notice} : {}), - ...(result.truncated ? {truncated: true} : {}), - }, - null, - 2, - ), - ) + outputResult(storeListJsonOutputSchema.encode(result)) return } renderTextResult(result) } -function truncationWarning(result: ListStoresResult): string { +function truncationWarning(result: StoreListResult): string { const organization = result.organization ? ` in ${result.organization.name}` : ' in this organization' return `Showing the ${STORE_LIST_LIMIT} most recent stores${organization}. More stores exist.` } -function renderTextResult(result: ListStoresResult): void { +function renderTextResult(result: StoreListResult): void { if (result.stores.length === 0) { outputInfo(emptyStateMessage(result)) return @@ -66,7 +53,7 @@ function renderOrganizationTable(stores: StoreListEntry[]): void { }) } -function emptyStateMessage(result: ListStoresResult): string { +function emptyStateMessage(result: StoreListResult): string { if (result.notice) { return [ 'No stores were returned for the current CLI session.', diff --git a/packages/store/src/cli/services/store/list/types.ts b/packages/store/src/cli/services/store/list/types.ts index 274a62c77f9..de78f18a5e2 100644 --- a/packages/store/src/cli/services/store/list/types.ts +++ b/packages/store/src/cli/services/store/list/types.ts @@ -1,22 +1,35 @@ -export interface StoreListEntry { - id?: string - store: string - createdAt: string - organizationId: string - organizationName: string - name?: string - type?: string -} +import {defineJsonOutputSchema, type InferJsonOutputSchema} from '@shopify/cli-kit/node/json-output-schema' +import {zod} from '@shopify/cli-kit/node/schema' -export interface StoreListOrganization { - id: string - name: string -} +const StoreListEntrySchema = zod.object({ + id: zod.string().optional(), + store: zod.string(), + createdAt: zod.string(), + organizationId: zod.string(), + organizationName: zod.string(), + name: zod.string().optional(), + type: zod.string().optional(), +}) -export interface ListStoresResult { - stores: StoreListEntry[] - source: 'organization' - organization?: StoreListOrganization - notice?: string - truncated?: boolean -} +const StoreListOrganizationSchema = zod.object({ + id: zod.string(), + name: zod.string(), +}) + +export const storeListJsonOutputSchema = defineJsonOutputSchema({ + name: 'StoreListResult', + schema: zod.object({ + stores: zod.array(StoreListEntrySchema), + organization: StoreListOrganizationSchema.optional(), + notice: zod.string().optional(), + truncated: zod.boolean().optional(), + }), + definitions: { + StoreListEntry: StoreListEntrySchema, + StoreListOrganization: StoreListOrganizationSchema, + }, +}) + +export type StoreListEntry = zod.infer +export type StoreListOrganization = zod.infer +export type StoreListResult = InferJsonOutputSchema