diff --git a/CHANGELOG.md b/CHANGELOG.md index dd22714ba4..9ee9ce1643 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This is the log of notable changes to EAS CLI and related packages. - [build-tools] Auto-upload embedded bundle after build when `EAS_UPDATE_EXPERIMENTAL_UPLOAD_EMBEDDED_BUNDLE` is set. ([#3767](https://github.com/expo/eas-cli/pull/3767) by [@gwdp](https://github.com/gwdp)) - [eas-cli] Add `eas update:embedded:upload` command. ([#3720](https://github.com/expo/eas-cli/pull/3720) by [@gwdp](https://github.com/gwdp)) +- [eas-cli] Add `eas update:embedded:view` command. ([#3721](https://github.com/expo/eas-cli/pull/3721) by [@gwdp](https://github.com/gwdp)) ### 🐛 Bug fixes diff --git a/packages/eas-cli/src/commands/update/embedded/view.ts b/packages/eas-cli/src/commands/update/embedded/view.ts new file mode 100644 index 0000000000..ff96e8d54d --- /dev/null +++ b/packages/eas-cli/src/commands/update/embedded/view.ts @@ -0,0 +1,82 @@ +import { Args, Errors } from '@oclif/core'; + +import EasCommand from '../../../commandUtils/EasCommand'; +import { EasJsonOnlyFlag } from '../../../commandUtils/flags'; +import { + EmbeddedUpdateFragment, + EmbeddedUpdateQuery, + isEmbeddedUpdateNotFoundError, +} from '../../../graphql/queries/EmbeddedUpdateQuery'; +import Log from '../../../log'; +import formatFields from '../../../utils/formatFields'; +import { enableJsonOutput, printJsonOnlyOutput } from '../../../utils/json'; + +export default class UpdateEmbeddedView extends EasCommand { + static override description = 'view details of an embedded update registered with EAS Update'; + + static override args = { + id: Args.string({ + required: true, + description: 'The ID of the embedded update (manifest UUID from app.manifest).', + }), + }; + + static override flags = { + ...EasJsonOnlyFlag, + }; + + static override contextDefinition = { + ...this.ContextOptions.ProjectId, + ...this.ContextOptions.LoggedIn, + }; + + async runAsync(): Promise { + const { + args: { id: embeddedUpdateId }, + flags: { json: jsonFlag }, + } = await this.parse(UpdateEmbeddedView); + + const { + projectId, + loggedIn: { graphqlClient }, + } = await this.getContextAsync(UpdateEmbeddedView, { nonInteractive: true }); + + if (jsonFlag) { + enableJsonOutput(); + } + + let embeddedUpdate; + try { + embeddedUpdate = await EmbeddedUpdateQuery.viewByIdAsync(graphqlClient, { + embeddedUpdateId, + appId: projectId, + }); + } catch (e: unknown) { + if (isEmbeddedUpdateNotFoundError(e)) { + Errors.error( + `No embedded update found with id "${embeddedUpdateId}" for this project. ` + + `Verify the id is correct and belongs to this app.`, + { exit: 1 } + ); + } + throw e; + } + + if (jsonFlag) { + printJsonOnlyOutput(embeddedUpdate); + return; + } + + Log.log(formatEmbeddedUpdate(embeddedUpdate)); + } +} + +export function formatEmbeddedUpdate(embeddedUpdate: EmbeddedUpdateFragment): string { + return formatFields([ + { label: 'ID', value: embeddedUpdate.id }, + { label: 'Platform', value: embeddedUpdate.platform.toLowerCase() }, + { label: 'Runtime version', value: embeddedUpdate.runtimeVersion }, + { label: 'Channel', value: embeddedUpdate.channel }, + { label: 'Created at', value: new Date(embeddedUpdate.createdAt).toLocaleString() }, + ]); +} diff --git a/packages/eas-cli/src/graphql/queries/EmbeddedUpdateQuery.ts b/packages/eas-cli/src/graphql/queries/EmbeddedUpdateQuery.ts new file mode 100644 index 0000000000..f130ee9827 --- /dev/null +++ b/packages/eas-cli/src/graphql/queries/EmbeddedUpdateQuery.ts @@ -0,0 +1,148 @@ +import { CombinedError } from '@urql/core'; +import gql from 'graphql-tag'; + +import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient'; +import { AppPlatform, EmbeddedUpdate } from '../generated'; +import { Connection } from '../../utils/relay'; +import { withErrorHandlingAsync } from '../client'; + +export function isEmbeddedUpdateNotFoundError(error: unknown): boolean { + return ( + error instanceof CombinedError && + error.graphQLErrors.some(e => e.extensions?.['errorCode'] === 'EMBEDDED_UPDATE_NOT_FOUND') + ); +} + +// Query result types are defined manually because the embeddedUpdates query fields +// are not yet included in the GraphQL codegen schema. +export type EmbeddedUpdateFragment = Pick< + EmbeddedUpdate, + 'id' | 'platform' | 'runtimeVersion' | 'channel' | 'createdAt' +>; + +type ViewEmbeddedUpdateByIdQueryResult = { + embeddedUpdates: { + byId: EmbeddedUpdateFragment; + }; +}; + +type ViewEmbeddedUpdateByIdQueryVariables = { + embeddedUpdateId: string; + appId: string; +}; + +type ViewEmbeddedUpdatesPaginatedQueryResult = { + app: { + byId: { + embeddedUpdatesPaginated: { + edges: { cursor: string; node: EmbeddedUpdateFragment }[]; + pageInfo: { + hasNextPage: boolean; + hasPreviousPage: boolean; + startCursor: string | null; + endCursor: string | null; + }; + }; + }; + }; +}; + +type ViewEmbeddedUpdatesPaginatedQueryVariables = { + appId: string; + first: number; + after?: string; + filter?: EmbeddedUpdateFilter; +}; + +export type EmbeddedUpdateFilter = { + platform?: AppPlatform; + runtimeVersion?: string; + channel?: string; +}; + +export const EmbeddedUpdateQuery = { + async viewByIdAsync( + graphqlClient: ExpoGraphqlClient, + { embeddedUpdateId, appId }: { embeddedUpdateId: string; appId: string } + ): Promise { + const data = await withErrorHandlingAsync( + graphqlClient + .query( + gql` + query ViewEmbeddedUpdateById($embeddedUpdateId: ID!, $appId: ID!) { + embeddedUpdates { + byId(embeddedUpdateId: $embeddedUpdateId, appId: $appId) { + id + platform + runtimeVersion + channel + createdAt + } + } + } + `, + { embeddedUpdateId, appId }, + { additionalTypenames: ['EmbeddedUpdate'] } + ) + .toPromise() + ); + return data.embeddedUpdates.byId; + }, + + async viewPaginatedAsync( + graphqlClient: ExpoGraphqlClient, + { + appId, + filter, + first, + after, + }: { + appId: string; + filter?: EmbeddedUpdateFilter; + first: number; + after?: string; + } + ): Promise> { + const data = await withErrorHandlingAsync( + graphqlClient + .query( + gql` + query ViewEmbeddedUpdatesPaginated( + $appId: String! + $first: Int! + $after: String + $filter: EmbeddedUpdateFilterInput + ) { + app { + byId(appId: $appId) { + id + embeddedUpdatesPaginated(first: $first, after: $after, filter: $filter) { + edges { + cursor + node { + id + platform + runtimeVersion + channel + createdAt + } + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + } + } + } + `, + { appId, first, after, filter }, + { additionalTypenames: ['EmbeddedUpdate'] } + ) + .toPromise() + ); + return data.app.byId.embeddedUpdatesPaginated; + }, +};