-
Notifications
You must be signed in to change notification settings - Fork 130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Migrate theme delete/update/publish to GraphQL #4623
Conversation
We detected some changes at either packages/*/src or packages/cli-kit/assets/cli-ruby/** and there are no updates in the .changeset. |
aae05b3
to
ca94b5a
Compare
const response = await request('POST', `/themes`, session, params) | ||
return buildTheme(response.json.theme) | ||
return buildTheme({ | ||
id: parseInt((theme.id as unknown as string).split('/').pop() as string, 10), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as a follow-up, what do you think of swapping over the Theme object here to use gids?
we need to convert back and forth between ints and gids in quite a few places. will we still need the plain int id anywhere after we're done migrating?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm kind of shocked we don't already have some composeGid
and parseGid
functions in the repo. Would be good to add them as a part of this transition I think. There's a parseGid
you can rip from Hydrogen if you want: https://github.com/Shopify/hydrogen/blob/main/packages/cli/src/lib/gid.ts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for this PR, @lucyxiang! It's heading in an excellent direction :)
I've left just a comments about keeping the API layer compatible with the Theme Access app. Otherwise, we might potentially impact CI/CD workflows and users using that as an authentication method.
Also, I believe we could ignore the admin_schema.graphql
file, following the reasoning in #4620 (comment).
Thanks again for this PR!
8a56f60
to
6ba04ee
Compare
Coverage report
Show new covered files 🐣
Show files with reduced coverage 🔻
Test suite run success1919 tests passing in 872 suites. Report generated by 🧪jest coverage report action from a65ac51 |
6ba04ee
to
0824a14
Compare
0824a14
to
ae33c65
Compare
We detected some changes at packages/*/src and there are no updates in the .changeset. |
ae33c65
to
2fb46d3
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No 🎩 yet but looking good! Small concern around the types that are getting generated, would love to confirm that.
const response = await request('POST', `/themes`, session, params) | ||
return buildTheme(response.json.theme) | ||
return buildTheme({ | ||
id: parseInt((theme.id as unknown as string).split('/').pop() as string, 10), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm kind of shocked we don't already have some composeGid
and parseGid
functions in the repo. Would be good to add them as a part of this transition I think. There's a parseGid
you can rip from Hydrogen if you want: https://github.com/Shopify/hydrogen/blob/main/packages/cli/src/lib/gid.ts
Quick question: for the mutations, should they be in something like |
4ab70cf
to
517c2bf
Compare
517c2bf
to
e0976e9
Compare
} else { | ||
// An unexpected error if neither theme nor userErrors are returned | ||
unexpectedGraphQLError('Failed to update theme') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure when this would happen (no execution error but also not getting back the specified fields). I was tempted to not have this else
but if it does happen, it's better we raise an error than not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍🏻 ideally adminRequestDoc
covers this case for all calls but we don't have to push that in this PR. I'm still convinced that the types in this repo aren't being generated quite right but I don't have time to poke at it yet. We'll just have to be extra defensive for now I think.
@@ -1,6 +1,6 @@ | |||
import {deleteThemes, renderDeprecatedArgsWarning} from './delete.js' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly renaming from here on down (all the changes in this directory)
e0976e9
to
555e722
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no 🎩 but code looks good!
return `gid://shopify/OnlineStoreTheme/${id}` | ||
} | ||
|
||
export function themeGidToId(gid: string): number { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: prefer parseGid
to match other systems.
(nit in the same vein: themeGid
=> composeThemeGid
but feel free to ignore)
} | ||
|
||
export function themeGidToId(gid: string): number { | ||
return parseInt((gid as unknown as string).split('/').pop() as string, 10) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe right now all of our resources should contain a numeric ID on the end of the GID but it's not necessarily a guarantee. I think parseInt()
can probably also handle query params (e.g. gid://shopify/Example/1?foo=3&bar=baz
but it feels wrong.
I know that this probably works in all scenarios but I wonder if it'd be better feedback if we threw an error if we ever received a GID that wasn't valid. Here's a regex that we've used in other projects:
return parseInt((gid as unknown as string).split('/').pop() as string, 10) | |
const id = `/${gid}`; | |
const matches = /\/(\w[\w-]*)(?:\?(.*))*$/.exec(id); | |
if (matches && matches[1] !== undefined) { | |
return matches[1]; | |
} | |
throw new Error(`Invalid GID: ${gid}`); |
And the one I mentioned before in Hydrogen: https://github.com/Shopify/hydrogen/blob/main/packages/cli/src/lib/gid.ts
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good to go with what hydrogen uses, it seems more foolproof which I like. Thank you for sharing Gray!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I missed this link in your previous comment, my bad 🙈 didn't mean to ignore it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once we have all the old REST code removed, I think we can remove most of the GID parsing / generation.
These can mostly be treated as opaque strings, with the exception of how the theme list
command outputs them, or passing in themes by id on the command line.
} else { | ||
// An unexpected error if neither theme nor userErrors are returned | ||
unexpectedGraphQLError('Failed to update theme') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍🏻 ideally adminRequestDoc
covers this case for all calls but we don't have to push that in this PR. I'm still convinced that the types in this repo aren't being generated quite right but I don't have time to poke at it yet. We'll just have to be extra defensive for now I think.
} | ||
|
||
export async function publishTheme(id: number, session: AdminSession): Promise<Theme | undefined> { | ||
return updateTheme(id, {role: 'main'}, session) | ||
export async function themePublish(id: number, session: AdminSession): Promise<Theme | undefined> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚲 🖌️ (nit) feel free to ignore completely, just sharing how I might have written this now that I see your new approach!
export async function themePublish(id: number, session: AdminSession): Promise<Theme | undefined> {
try {
const {themePublish} = await adminRequestDoc(ThemePublish, session, {id: themeGid(id)})
if (!themePublish) throw new Error('')
const {theme, userErrors} = themePublish
if (userErrors.length) {
throw new Error(userErrors.map((error) => error.message).join(', '))
}
if (!theme) throw new Error('')
return buildTheme({
id: themeGidToId(theme.id),
name: theme.name,
role: theme.role.toLowerCase(),
})
} catch (error: unknown) {
if (error instanceof Error) {
throw new AbortError(error.message)
}
throw new AbortError('An unknown error occurred')
}
}
I like the try/catch here because it means we can just throw
when we don't like the state and we don't end up in nested if/else statements.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I used a mix of yours and guilherme's suggestion. I don't love throwing Error('')
and then populating it later , but agreed with less if/else statements.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for this PR, @lucyxiang! LGTM and works as expected as well 🎩 I've left two minor comments that I'd like to know your thoughts :)
Thanks again for this PR!
4dc5b4a
to
6eaa4ac
Compare
6eaa4ac
to
a65ac51
Compare
Differences in type declarationsWe detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:
New type declarationspackages/cli-kit/dist/cli/api/graphql/admin/generated/theme_delete.d.tsimport * as Types from './types.js';
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
export type ThemeDeleteMutationVariables = Types.Exact<{
id: Types.Scalars['ID']['input'];
}>;
export type ThemeDeleteMutation = {
themeDelete?: {
deletedThemeId?: string | null;
userErrors: {
field?: string[] | null;
message: string;
}[];
} | null;
};
export declare const ThemeDelete: DocumentNode<ThemeDeleteMutation, Types.Exact<{
id: Types.Scalars['ID']['input'];
}>>;
packages/cli-kit/dist/cli/api/graphql/admin/generated/theme_publish.d.tsimport * as Types from './types.js';
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
export type ThemePublishMutationVariables = Types.Exact<{
id: Types.Scalars['ID']['input'];
}>;
export type ThemePublishMutation = {
themePublish?: {
theme?: {
id: string;
name: string;
role: Types.ThemeRole;
} | null;
userErrors: {
field?: string[] | null;
message: string;
}[];
} | null;
};
export declare const ThemePublish: DocumentNode<ThemePublishMutation, Types.Exact<{
id: Types.Scalars['ID']['input'];
}>>;
packages/cli-kit/dist/cli/api/graphql/admin/generated/theme_update.d.tsimport * as Types from './types.js';
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
export type ThemeUpdateMutationVariables = Types.Exact<{
id: Types.Scalars['ID']['input'];
input: Types.OnlineStoreThemeInput;
}>;
export type ThemeUpdateMutation = {
themeUpdate?: {
theme?: {
id: string;
name: string;
role: Types.ThemeRole;
} | null;
userErrors: {
field?: string[] | null;
message: string;
}[];
} | null;
};
export declare const ThemeUpdate: DocumentNode<ThemeUpdateMutation, Types.Exact<{
id: Types.Scalars['ID']['input'];
input: Types.OnlineStoreThemeInput;
}>>;
Existing type declarationspackages/cli-kit/dist/public/node/themes/api.d.ts@@ -9,14 +9,6 @@ export declare function fetchThemeAsset(id: number, key: Key, session: AdminSess
export declare function deleteThemeAsset(id: number, key: Key, session: AdminSession): Promise<boolean>;
export declare function bulkUploadThemeAssets(id: number, assets: AssetParams[], session: AdminSession): Promise<Result[]>;
export declare function fetchChecksums(id: number, session: AdminSession): Promise<Checksum[]>;
-interface UpgradeThemeOptions {
- fromTheme: number;
- toTheme: number;
- script?: string;
- session: AdminSession;
-}
-export declare function upgradeTheme(upgradeOptions: UpgradeThemeOptions): Promise<Theme | undefined>;
-export declare function updateTheme(id: number, params: ThemeParams, session: AdminSession): Promise<Theme | undefined>;
-export declare function publishTheme(id: number, session: AdminSession): Promise<Theme | undefined>;
-export declare function deleteTheme(id: number, session: AdminSession): Promise<Theme | undefined>;
-export {};
\ No newline at end of file
+export declare function themeUpdate(id: number, params: ThemeParams, session: AdminSession): Promise<Theme | undefined>;
+export declare function themePublish(id: number, session: AdminSession): Promise<Theme | undefined>;
+export declare function themeDelete(id: number, session: AdminSession): Promise<boolean | undefined>;
\ No newline at end of file
packages/cli-kit/dist/public/node/themes/utils.d.ts@@ -4,4 +4,6 @@ export declare const LIVE_THEME_ROLE = "live";
export declare const UNPUBLISHED_THEME_ROLE = "unpublished";
export type Role = typeof DEVELOPMENT_THEME_ROLE | typeof LIVE_THEME_ROLE | typeof UNPUBLISHED_THEME_ROLE;
export declare function isDevelopmentTheme(theme: Theme): boolean;
-export declare function promptThemeName(message: string): Promise<string>;
\ No newline at end of file
+export declare function promptThemeName(message: string): Promise<string>;
+export declare function composeThemeGid(id: number): string;
+export declare function parseGid(gid: string): number;
\ No newline at end of file
|
/shipit |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the delay.
WHY are these changes introduced?
Following the introduction of theme admin GraphQL endpoints, port the CLI to use GraphQL.
Extending what @catlee started here #4541
WHAT is this pull request doing?
Migrate
themeDelete
,themeUpdate
, andthemePublish
to use the admin graphql for both regular sessions and theme access sessions. Renamed uses ofdelete/update/publishTheme
tothemeDelete/Update/Publish
for consistency.Change themeDelete to return boolean (true on success, false on failure) instead of a theme.
To ship after Chris' code gen PR (shipped).
How to test your changes?
shopify theme delete
shopify theme publish
shopify theme rename
Measuring impact
How do we know this change was effective? Please choose one:
Checklist