Skip to content
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

[stable] Fix bug with contracts and missing fields #5483

Merged
merged 3 commits into from
Mar 4, 2025

Conversation

isaacroldan
Copy link
Contributor

WHY are these changes introduced?

The current implementation strips first-class fields before JSON schema validation, which prevents these fields from being used later in the configuration.

This change ensures that common base properties are still included in the extension config (they'll be stripped before deployment)

WHAT is this pull request doing?

  • Introduces JsonSchemaBaseProperties to define common base properties for all JSON Schema contracts
  • Adds these base properties to the contract's properties during validation
  • Removes the stripping of first-class fields before JSON schema validation
  • Ensures base properties like type, handle, uid, path, and extensions are preserved in the configuration

How to test your changes?

  1. Create an admin_link extension, be sure to have a handle in your toml that is completely different from the name attribute. Example toml:
[[extensions]]
name = "My Extension"
handle = "support-link"
type = "admin_link"

[[extensions.targeting]]
target = "admin.app.support.link"
url = "app://help"
  1. Without this PR, if you run app info you'll see an extension identified as my-extension. Because the handle wasn't loaded properly.
  2. With this PR, running app info you'll see the same extension identified as support-link, because now the handle was loaded correctly.
  3. Make a deploy with this new version to validate that we are still sending the expected attributes.

Measuring impact

  • n/a - this doesn't need measurement, e.g. a linting rule or a bug-fix

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've considered possible documentation changes

Copy link
Contributor

github-actions bot commented Mar 4, 2025

Coverage report

St.
Category Percentage Covered / Total
🟡 Statements 75.99% 9181/12082
🟡 Branches 71.12% 4492/6316
🟡 Functions 75.54% 2393/3168
🟡 Lines 76.55% 8672/11328

Test suite run success

2084 tests passing in 924 suites.

Report generated by 🧪jest coverage report action from f80b326

@isaacroldan isaacroldan changed the base branch from main to stable/3.76 March 4, 2025 16:37
@isaacroldan isaacroldan marked this pull request as ready for review March 4, 2025 16:37
@isaacroldan isaacroldan requested review from a team as code owners March 4, 2025 16:37
Copy link
Contributor

github-actions bot commented Mar 4, 2025

Differences in type declarations

We 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:

  • Some seemingly private modules might be re-exported through public modules.
  • If the branch is behind main you might see odd diffs, rebase main into this branch.

New type declarations

We found no new type declarations in this PR

Existing type declarations

packages/cli-kit/dist/private/node/api.d.ts
@@ -1,41 +1,18 @@
 import { Headers } from 'form-data';
 export type API = 'admin' | 'storefront-renderer' | 'partners' | 'business-platform' | 'app-management';
 export declare const allAPIs: API[];
-export type NetworkRetryBehaviour = {
-    useNetworkLevelRetry: true;
-    maxRetryTimeMs: number;
-} | {
-    useNetworkLevelRetry: false;
-};
-type RequestOptions<T> = {
+interface RequestOptions<T> {
     request: () => Promise<T>;
     url: string;
-} & NetworkRetryBehaviour;
+}
 export declare function simpleRequestWithDebugLog<T extends {
     headers: Headers;
     status: number;
-}>(requestOptions: RequestOptions<T>, errorHandler?: (error: unknown, requestId: string | undefined) => unknown): Promise<T>;
-/**
- * Makes a HTTP request to some API, retrying if response headers indicate a retryable error.
- *
- * If a request fails with a 429, the retry-after header determines a delay before an automatic retry is performed.
- *
- * If unauthorizedHandler is provided, then it will be called in the case of a 401 and a retry performed. This allows
- * for a token refresh for instance.
- *
- * If there's a network error, e.g. DNS fails to resolve, then API calls are automatically retried.
- *
- * @param request - A function that returns a promise of the response
- * @param url - The URL to request
- * @param errorHandler - A function that handles errors
- * @param unauthorizedHandler - A function that handles unauthorized errors
- * @param retryOptions - Options for the retry
- * @returns The response from the request
- */
+}>({ request, url }: RequestOptions<T>, errorHandler?: (error: unknown, requestId: string | undefined) => unknown): Promise<T>;
 export declare function retryAwareRequest<T extends {
     headers: Headers;
     status: number;
-}>(requestOptions: RequestOptions<T>, errorHandler?: (error: unknown, requestId: string | undefined) => unknown, unauthorizedHandler?: () => Promise<void>, retryOptions?: {
+}>({ request, url }: RequestOptions<T>, errorHandler?: (error: unknown, requestId: string | undefined) => unknown, unauthorizedHandler?: () => Promise<void>, retryOptions?: {
     limitRetriesTo?: number;
     defaultDelayMs?: number;
     scheduleDelay: (fn: () => void, delay: number) => void;
packages/cli-kit/dist/private/node/constants.d.ts
@@ -32,8 +32,6 @@ export declare const environmentVariables: {
     themeKitAccessDomain: string;
     json: string;
     neverUsePartnersApi: string;
-    skipNetworkLevelRetry: string;
-    maxRequestTimeForNetworkCalls: string;
 };
 export declare const defaultThemeKitAccessDomain = "theme-kit-access.shopifyapps.com";
 export declare const systemEnvironmentVariables: {
packages/cli-kit/dist/public/node/environment.d.ts
@@ -55,24 +55,4 @@ export declare function jsonOutputEnabled(environment?: NodeJS.ProcessEnv): bool
  *
  * @returns True if the SHOPIFY_CLI_NEVER_USE_PARTNERS_API environment variable is set.
  */
-export declare function blockPartnersAccess(): boolean;
-/**
- * If true, the CLI should not use the network level retry.
- *
- * If there is an error when calling a network API that looks like a DNS or connectivity issue, the CLI will by default
- * automatically retry the request.
- *
- * @param environment - Process environment variables.
- * @returns True if the SHOPIFY_CLI_SKIP_NETWORK_LEVEL_RETRY environment variable is set.
- */
-export declare function skipNetworkLevelRetry(environment?: NodeJS.ProcessEnv): boolean;
-/**
- * Returns the default maximum request time for network calls in milliseconds.
- *
- * After this long, API requests may be cancelled by an AbortSignal. The limit can be overridden by setting the
- * SHOPIFY_CLI_MAX_REQUEST_TIME_FOR_NETWORK_CALLS environment variable.
- *
- * @param environment - Process environment variables.
- * @returns The maximum request time in milliseconds.
- */
-export declare function maxRequestTimeForNetworkCallsMs(environment?: NodeJS.ProcessEnv): number;
\ No newline at end of file
+export declare function blockPartnersAccess(): boolean;
\ No newline at end of file
packages/cli-kit/dist/public/node/http.d.ts
@@ -1,4 +1,3 @@
-import { NetworkRetryBehaviour } from '../../private/node/api.js';
 import FormData from 'form-data';
 import { RequestInfo, RequestInit, Response } from 'node-fetch';
 export { FetchError, Request, Response } from 'node-fetch';
@@ -8,43 +7,6 @@ export { FetchError, Request, Response } from 'node-fetch';
  * @returns A FormData object.
  */
 export declare function formData(): FormData;
-type AbortSignal = RequestInit['signal'];
-type PresetFetchBehaviour = 'default' | 'non-blocking' | 'slow-request';
-type AutomaticCancellationBehaviour = {
-    useAbortSignal: true;
-    timeoutMs: number;
-} | {
-    useAbortSignal: false;
-} | {
-    useAbortSignal: AbortSignal | (() => AbortSignal);
-};
-type RequestBehaviour = NetworkRetryBehaviour & AutomaticCancellationBehaviour;
-type RequestModeInput = PresetFetchBehaviour | RequestBehaviour;
-/**
- * Specify the behaviour of a network request.
- *
- * - default: Requests are automatically retried, and are subject to automatic cancellation if they're taking too long.
- * This is generally desirable.
- * - non-blocking: Requests are not retried if they fail with a network error, and are automatically cancelled if
- * they're taking too long. This is good for throwaway requests, like polling or tracking.
- * - slow-request: Requests are not retried if they fail with a network error, and are not automatically cancelled.
- * This is good for slow requests that should be give the chance to complete, and are unlikely to be safe to retry.
- *
- * Some request behaviours may be de-activated by the environment, and this function takes care of that concern. You
- * can also provide a customised request behaviour.
- *
- * @param preset - The preset to use.
- * @param env - Process environment variables.
- * @returns A request behaviour object.
- */
-export declare function requestMode(preset?: RequestModeInput, env?: NodeJS.ProcessEnv): RequestBehaviour;
-/**
- * Create an AbortSignal for automatic request cancellation, from a request behaviour.
- *
- * @param behaviour - The request behaviour.
- * @returns An AbortSignal.
- */
-export declare function abortSignalFromRequestBehaviour(behaviour: RequestBehaviour): AbortSignal;
 /**
  * An interface that abstracts way node-fetch. When Node has built-in
  * support for "fetch" in the standard library, we can drop the node-fetch
@@ -53,28 +15,21 @@ export declare function abortSignalFromRequestBehaviour(behaviour: RequestBehavi
  * they are consistent with the Web API so if we drop node-fetch in the future
  * it won't require changes from the callers.
  *
- * The CLI's fetch function supports special behaviours, like automatic retries. These are disabled by default through
- * this function.
- *
  * @param url - This defines the resource that you wish to fetch.
  * @param init - An object containing any custom settings that you want to apply to the request.
- * @param preferredBehaviour - A request behaviour object that overrides the default behaviour.
  * @returns A promise that resolves with the response.
  */
-export declare function fetch(url: RequestInfo, init?: RequestInit, preferredBehaviour?: RequestModeInput): Promise<Response>;
+export declare function fetch(url: RequestInfo, init?: RequestInit): Promise<Response>;
 /**
  * A fetch function to use with Shopify services. The function ensures the right
  * TLS configuragion is used based on the environment in which the service is running
- * (e.g. Spin). NB: headers/auth are the responsibility of the caller.
- *
- * By default, the CLI's fetch function's special behaviours, like automatic retries, are enabled.
+ * (e.g. Spin).
  *
  * @param url - This defines the resource that you wish to fetch.
  * @param init - An object containing any custom settings that you want to apply to the request.
- * @param preferredBehaviour - A request behaviour object that overrides the default behaviour.
  * @returns A promise that resolves with the response.
  */
-export declare function shopifyFetch(url: RequestInfo, init?: RequestInit, preferredBehaviour?: RequestModeInput): Promise<Response>;
+export declare function shopifyFetch(url: RequestInfo, init?: RequestInit): Promise<Response>;
 /**
  * Download a file from a URL to a local path.
  *
packages/cli-kit/dist/private/node/session/schema.d.ts
@@ -9,15 +9,15 @@ declare const IdentityTokenSchema: zod.ZodObject<{
     scopes: zod.ZodArray<zod.ZodString, "many">;
     userId: zod.ZodString;
 }, "strip", zod.ZodTypeAny, {
+    scopes: string[];
     accessToken: string;
     refreshToken: string;
-    scopes: string[];
     expiresAt: Date;
     userId: string;
 }, {
+    scopes: string[];
     accessToken: string;
     refreshToken: string;
-    scopes: string[];
     userId: string;
     expiresAt?: unknown;
 }>;
@@ -29,12 +29,12 @@ declare const ApplicationTokenSchema: zod.ZodObject<{
     expiresAt: zod.ZodEffects<zod.ZodDate, Date, unknown>;
     scopes: zod.ZodArray<zod.ZodString, "many">;
 }, "strip", zod.ZodTypeAny, {
-    accessToken: string;
     scopes: string[];
+    accessToken: string;
     expiresAt: Date;
 }, {
-    accessToken: string;
     scopes: string[];
+    accessToken: string;
     expiresAt?: unknown;
 }>;
 /**
@@ -58,15 +58,15 @@ export declare const SessionSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{
         scopes: zod.ZodArray<zod.ZodString, "many">;
         userId: zod.ZodString;
     }, "strip", zod.ZodTypeAny, {
+        scopes: string[];
         accessToken: string;
         refreshToken: string;
-        scopes: string[];
         expiresAt: Date;
         userId: string;
     }, {
+        scopes: string[];
         accessToken: string;
         refreshToken: string;
-        scopes: string[];
         userId: string;
         expiresAt?: unknown;
     }>;
@@ -79,65 +79,65 @@ export declare const SessionSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{
         expiresAt: zod.ZodEffects<zod.ZodDate, Date, unknown>;
         scopes: zod.ZodArray<zod.ZodString, "many">;
     }, "strip", zod.ZodTypeAny, {
-        accessToken: string;
         scopes: string[];
+        accessToken: string;
         expiresAt: Date;
     }, {
-        accessToken: string;
         scopes: string[];
+        accessToken: string;
         expiresAt?: unknown;
     }>, zod.objectOutputType<{}, zod.ZodObject<{
         accessToken: zod.ZodString;
         expiresAt: zod.ZodEffects<zod.ZodDate, Date, unknown>;
         scopes: zod.ZodArray<zod.ZodString, "many">;
     }, "strip", zod.ZodTypeAny, {
-        accessToken: string;
         scopes: string[];
+        accessToken: string;
         expiresAt: Date;
     }, {
-        accessToken: string;
         scopes: string[];
+        accessToken: string;
         expiresAt?: unknown;
     }>, "strip">, zod.objectInputType<{}, zod.ZodObject<{
         accessToken: zod.ZodString;
         expiresAt: zod.ZodEffects<zod.ZodDate, Date, unknown>;
         scopes: zod.ZodArray<zod.ZodString, "many">;
     }, "strip", zod.ZodTypeAny, {
-        accessToken: string;
         scopes: string[];
+        accessToken: string;
         expiresAt: Date;
     }, {
-        accessToken: string;
         scopes: string[];
+        accessToken: string;
         expiresAt?: unknown;
     }>, "strip">>;
 }, "strip", zod.ZodTypeAny, {
     identity: {
+        scopes: string[];
         accessToken: string;
         refreshToken: string;
-        scopes: string[];
         expiresAt: Date;
         userId: string;
     };
     applications: {} & {
         [k: string]: {
-            accessToken: string;
             scopes: string[];
+            accessToken: string;
             expiresAt: Date;
         };
     };
 }, {
     identity: {
+        scopes: string[];
         accessToken: string;
         refreshToken: string;
-        scopes: string[];
         userId: string;
         expiresAt?: unknown;
     };
     applications: {} & {
         [k: string]: {
-            accessToken: string;
             scopes: string[];
+            accessToken: string;
             expiresAt?: unknown;
         };
     };
@@ -154,15 +154,15 @@ export declare const SessionSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{
         scopes: zod.ZodArray<zod.ZodString, "many">;
         userId: zod.ZodString;
     }, "strip", zod.ZodTypeAny, {
+        scopes: string[];
         accessToken: string;
         refreshToken: string;
-        scopes: string[];
         expiresAt: Date;
         userId: string;
     }, {
+        scopes: string[];
         accessToken: string;
         refreshToken: string;
-        scopes: string[];
         userId: string;
         expiresAt?: unknown;
     }>;
@@ -175,65 +175,65 @@ export declare const SessionSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{
         expiresAt: zod.ZodEffects<zod.ZodDate, Date, unknown>;
         scopes: zod.ZodArray<zod.ZodString, "many">;
     }, "strip", zod.ZodTypeAny, {
-        accessToken: string;
         scopes: string[];
+        accessToken: string;
         expiresAt: Date;
     }, {
-        accessToken: string;
         scopes: string[];
+        accessToken: string;
         expiresAt?: unknown;
     }>, zod.objectOutputType<{}, zod.ZodObject<{
         accessToken: zod.ZodString;
         expiresAt: zod.ZodEffects<zod.ZodDate, Date, unknown>;
         scopes: zod.ZodArray<zod.ZodString, "many">;
     }, "strip", zod.ZodTypeAny, {
-        accessToken: string;
         scopes: string[];
+        accessToken: string;
         expiresAt: Date;
     }, {
-        accessToken: string;
         scopes: string[];
+        accessToken: string;
         expiresAt?: unknown;
     }>, "strip">, zod.objectInputType<{}, zod.ZodObject<{
         accessToken: zod.ZodString;
         expiresAt: zod.ZodEffects<zod.ZodDate, Date, unknown>;
         scopes: zod.ZodArray<zod.ZodString, "many">;
     }, "strip", zod.ZodTypeAny, {
-        accessToken: string;
         scopes: string[];
+        accessToken: string;
         expiresAt: Date;
     }, {
-        accessToken: string;
         scopes: string[];
+        accessToken: string;
         expiresAt?: unknown;
     }>, "strip">>;
 }, "strip", zod.ZodTypeAny, {
     identity: {
+        scopes: string[];
         accessToken: string;
         refreshToken: string;
-        scopes: string[];
         expiresAt: Date;
         userId: string;
     };
     applications: {} & {
         [k: string]: {
-            accessToken: string;
             scopes: string[];
+            accessToken: string;
             expiresAt: Date;
         };
     };
 }, {
     identity: {
+        scopes: string[];
         accessToken: string;
         refreshToken: string;
-        scopes: string[];
         userId: string;
         expiresAt?: unknown;
     };
     applications: {} & {
         [k: string]: {
-            accessToken: string;
             scopes: string[];
+            accessToken: string;
             expiresAt?: unknown;
         };
     };
@@ -250,15 +250,15 @@ export declare const SessionSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{
         scopes: zod.ZodArray<zod.ZodString, "many">;
         userId: zod.ZodString;
     }, "strip", zod.ZodTypeAny, {
+        scopes: string[];
         accessToken: string;
         refreshToken: string;
-        scopes: string[];
         expiresAt: Date;
         userId: string;
     }, {
+        scopes: string[];
         accessToken: string;
         refreshToken: string;
-        scopes: string[];
         userId: string;
         expiresAt?: unknown;
     }>;
@@ -271,65 +271,65 @@ export declare const SessionSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{
         expiresAt: zod.ZodEffects<zod.ZodDate, Date, unknown>;
         scopes: zod.ZodArray<zod.ZodString, "many">;
     }, "strip", zod.ZodTypeAny, {
-        accessToken: string;
         scopes: string[];
+        accessToken: string;
         expiresAt: Date;
     }, {
-        accessToken: string;
         scopes: string[];
+        accessToken: string;
         expiresAt?: unknown;
     }>, zod.objectOutputType<{}, zod.ZodObject<{
         accessToken: zod.ZodString;
         expiresAt: zod.ZodEffects<zod.ZodDate, Date, unknown>;
         scopes: zod.ZodArray<zod.ZodString, "many">;
     }, "strip", zod.ZodTypeAny, {
-        accessToken: string;
         scopes: string[];
+        accessToken: string;
         expiresAt: Date;
     }, {
-        accessToken: string;
         scopes: string[];
+        accessToken: string;
         expiresAt?: unknown;
     }>, "strip">, zod.objectInputType<{}, zod.ZodObject<{
         accessToken: zod.ZodString;
         expiresAt: zod.ZodEffects<zod.ZodDate, Date, unknown>;
         scopes: zod.ZodArray<zod.ZodString, "many">;
     }, "strip", zod.ZodTypeAny, {
-        accessToken: string;
         scopes: string[];
+        accessToken: string;
         expiresAt: Date;
     }, {
-        accessToken: string;
         scopes: string[];
+        accessToken: string;
         expiresAt?: unknown;
     }>, "strip">>;
 }, "strip", zod.ZodTypeAny, {
     identity: {
+        scopes: string[];
         accessToken: string;
         refreshToken: string;
-        scopes: string[];
         expiresAt: Date;
         userId: string;
     };
     applications: {} & {
         [k: string]: {
-            accessToken: string;
             scopes: string[];
+            accessToken: string;
             expiresAt: Date;
         };
     };
 }, {
     identity: {
+        scopes: string[];
         accessToken: string;
         refreshToken: string;
-        scopes: string[];
         userId: string;
         expiresAt?: unknown;
     };
     applications: {} & {
         [k: string]: {
-            accessToken: string;
             scopes: string[];
+            accessToken: string;
             expiresAt?: unknown;
         };
     };

@isaacroldan isaacroldan merged commit 07866b9 into stable/3.76 Mar 4, 2025
28 checks passed
@isaacroldan isaacroldan deleted the fix-bug-with-contracts-stable branch March 4, 2025 16:50
@patryk-smc
Copy link

Thank you 💚

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants