diff --git a/bin/get-graphql-schemas.js b/bin/get-graphql-schemas.js index 31685438520..598c0c7f38f 100755 --- a/bin/get-graphql-schemas.js +++ b/bin/get-graphql-schemas.js @@ -44,7 +44,13 @@ const schemas = [ pathToFile: 'areas/core/shopify/db/graphql/webhooks_schema_unstable_public.graphql', localPath: './packages/app/src/cli/api/graphql/webhooks/webhooks_schema.graphql', branch: 'dd', - } + }, + { + repo: 'shopify', + pathToFile: 'areas/core/shopify/db/graphql/functions_cli_api_schema_unstable_public.graphql', + localPath: './packages/app/src/cli/api/graphql/functions/functions_cli_schema.graphql', + branch: 'dd', + }, ] function runCommand(command, args) { diff --git a/graphql.config.ts b/graphql.config.ts index e101f3635d2..c3b9243b3ec 100644 --- a/graphql.config.ts +++ b/graphql.config.ts @@ -82,5 +82,6 @@ export default { appManagement: projectFactory('app-management', 'app_management_schema.graphql'), admin: projectFactory('admin', 'admin_schema.graphql', 'cli-kit'), webhooks: projectFactory('webhooks', 'webhooks_schema.graphql'), + functions: projectFactory('functions', 'functions_cli_schema.graphql', 'app'), }, } diff --git a/packages/app/project.json b/packages/app/project.json index 696d41e380a..429b55fb968 100644 --- a/packages/app/project.json +++ b/packages/app/project.json @@ -81,7 +81,8 @@ "{projectRoot}/src/cli/api/graphql/business-platform-organizations/generated/**/*.ts", "{projectRoot}/src/cli/api/graphql/app-dev/generated/**/*.ts", "{projectRoot}/src/cli/api/graphql/app-management/generated/**/*.ts", - "{projectRoot}/src/cli/api/graphql/webhooks/generated/**/*.ts" + "{projectRoot}/src/cli/api/graphql/webhooks/generated/**/*.ts", + "{projectRoot}/src/cli/api/graphql/functions/generated/**/*.ts" ], "options": { "commands": [ @@ -90,7 +91,8 @@ "pnpm eslint 'src/cli/api/graphql/business-platform-organizations/generated/**/*.{ts,tsx}' --fix", "pnpm eslint 'src/cli/api/graphql/app-dev/generated/**/*.{ts,tsx}' --fix", "pnpm eslint 'src/cli/api/graphql/app-management/generated/**/*.{ts,tsx}' --fix", - "pnpm eslint 'src/cli/api/graphql/webhooks/generated/**/*.{ts,tsx}' --fix" + "pnpm eslint 'src/cli/api/graphql/webhooks/generated/**/*.{ts,tsx}' --fix", + "pnpm eslint 'src/cli/api/graphql/functions/generated/**/*.{ts,tsx}' --fix" ], "cwd": "packages/app" } @@ -161,6 +163,17 @@ "cwd": "{workspaceRoot}" } }, + "graphql-codegen:generate:functions": { + "executor": "nx:run-commands", + "inputs": ["{workspaceRoot}/graphql.config.ts", "{projectRoot}/src/cli/api/graphql/functions/**/*.graphql"], + "outputs": ["{projectRoot}/src/cli/api/graphql/functions/generated/**/*.ts"], + "options": { + "commands": [ + "pnpm exec graphql-codegen --project=functions" + ], + "cwd": "{workspaceRoot}" + } + }, "graphql-codegen:postfix": { "executor": "nx:run-commands", "dependsOn": [ @@ -169,7 +182,8 @@ "graphql-codegen:generate:business-platform-organizations", "graphql-codegen:generate:app-dev", "graphql-codegen:generate:app-management", - "graphql-codegen:generate:webhooks" + "graphql-codegen:generate:webhooks", + "graphql-codegen:generate:functions" ], "inputs": [{ "dependentTasksOutputFiles": "**/*.ts" }], "outputs": [ @@ -178,7 +192,8 @@ "{projectRoot}/src/cli/api/graphql/business-platform-organizations/generated/**/*.ts", "{projectRoot}/src/cli/api/graphql/app-dev/generated/**/*.ts", "{projectRoot}/src/cli/api/graphql/app-management/generated/**/*.ts", - "{projectRoot}/src/cli/api/graphql/webhooks/generated/**/*.ts" + "{projectRoot}/src/cli/api/graphql/webhooks/generated/**/*.ts", + "{projectRoot}/src/cli/api/graphql/functions/generated/**/*.ts" ], "options": { "commands": [ @@ -187,7 +202,8 @@ "find ./packages/app/src/cli/api/graphql/business-platform-organizations/generated/ -type f -name '*.ts' -exec sh -c 'sed -i \"\" \"s|import \\* as Types from '\\''./types'\\'';|import \\* as Types from '\\''./types.js'\\'';|g; s|export const \\([A-Za-z0-9_]*\\)Document =|export const \\1 =|g\" \"$0\"' {} \\;", "find ./packages/app/src/cli/api/graphql/app-dev/generated/ -type f -name '*.ts' -exec sh -c 'sed -i \"\" \"s|import \\* as Types from '\\''./types'\\'';|import \\* as Types from '\\''./types.js'\\'';|g; s|export const \\([A-Za-z0-9_]*\\)Document =|export const \\1 =|g\" \"$0\"' {} \\;", "find ./packages/app/src/cli/api/graphql/app-management/generated/ -type f -name '*.ts' -exec sh -c 'sed -i \"\" \"s|import \\* as Types from '\\''./types'\\'';|import \\* as Types from '\\''./types.js'\\'';|g; s|export const \\([A-Za-z0-9_]*\\)Document =|export const \\1 =|g\" \"$0\"' {} \\;", - "find ./packages/app/src/cli/api/graphql/webhooks/generated/ -type f -name '*.ts' -exec sh -c 'sed -i \"\" \"s|import \\* as Types from '\\''./types'\\'';|import \\* as Types from '\\''./types.js'\\'';|g; s|export const \\([A-Za-z0-9_]*\\)Document =|export const \\1 =|g\" \"$0\"' {} \\;" + "find ./packages/app/src/cli/api/graphql/webhooks/generated/ -type f -name '*.ts' -exec sh -c 'sed -i \"\" \"s|import \\* as Types from '\\''./types'\\'';|import \\* as Types from '\\''./types.js'\\'';|g; s|export const \\([A-Za-z0-9_]*\\)Document =|export const \\1 =|g\" \"$0\"' {} \\;", + "find ./packages/app/src/cli/api/graphql/functions/generated/ -type f -name '*.ts' -exec sh -c 'sed -i \"\" \"s|import \\* as Types from '\\''./types'\\'';|import \\* as Types from '\\''./types.js'\\'';|g; s|export const \\([A-Za-z0-9_]*\\)Document =|export const \\1 =|g\" \"$0\"' {} \\;" ], "cwd": "{workspaceRoot}" } diff --git a/packages/app/src/cli/api/graphql/functions/api_schema_definition.ts b/packages/app/src/cli/api/graphql/functions/api_schema_definition.ts index 09a3806b3de..bf6be02a5dc 100644 --- a/packages/app/src/cli/api/graphql/functions/api_schema_definition.ts +++ b/packages/app/src/cli/api/graphql/functions/api_schema_definition.ts @@ -9,9 +9,3 @@ export const ApiSchemaDefinitionQuery = gql` export interface ApiSchemaDefinitionQuerySchema { definition: string | null } - -export interface ApiSchemaDefinitionQueryVariables { - apiKey: string - version: string - type: string -} diff --git a/packages/app/src/cli/api/graphql/functions/generated/schema-definition-by-api-type.ts b/packages/app/src/cli/api/graphql/functions/generated/schema-definition-by-api-type.ts new file mode 100644 index 00000000000..59d66f3461a --- /dev/null +++ b/packages/app/src/cli/api/graphql/functions/generated/schema-definition-by-api-type.ts @@ -0,0 +1,74 @@ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ +import * as Types from './types.js' + +import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core' + +export type SchemaDefinitionByApiTypeQueryVariables = Types.Exact<{ + type: Types.Scalars['String']['input'] + version: Types.Scalars['String']['input'] +}> + +export type SchemaDefinitionByApiTypeQuery = {api?: {schema?: {definition: string} | null} | null} + +export const SchemaDefinitionByApiType = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'query', + name: {kind: 'Name', value: 'SchemaDefinitionByApiType'}, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: {kind: 'Variable', name: {kind: 'Name', value: 'type'}}, + type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}}, + }, + { + kind: 'VariableDefinition', + variable: {kind: 'Variable', name: {kind: 'Name', value: 'version'}}, + type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}}, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: {kind: 'Name', value: 'api'}, + arguments: [ + { + kind: 'Argument', + name: {kind: 'Name', value: 'type'}, + value: {kind: 'Variable', name: {kind: 'Name', value: 'type'}}, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: {kind: 'Name', value: 'schema'}, + arguments: [ + { + kind: 'Argument', + name: {kind: 'Name', value: 'version'}, + value: {kind: 'Variable', name: {kind: 'Name', value: 'version'}}, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + {kind: 'Field', name: {kind: 'Name', value: 'definition'}}, + {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, + ], + }, + }, + {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode diff --git a/packages/app/src/cli/api/graphql/functions/generated/schema-definition-by-target.ts b/packages/app/src/cli/api/graphql/functions/generated/schema-definition-by-target.ts new file mode 100644 index 00000000000..f7dadf4593a --- /dev/null +++ b/packages/app/src/cli/api/graphql/functions/generated/schema-definition-by-target.ts @@ -0,0 +1,84 @@ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ +import * as Types from './types.js' + +import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core' + +export type SchemaDefinitionByTargetQueryVariables = Types.Exact<{ + handle: Types.Scalars['String']['input'] + version: Types.Scalars['String']['input'] +}> + +export type SchemaDefinitionByTargetQuery = {target?: {api: {schema?: {definition: string} | null}} | null} + +export const SchemaDefinitionByTarget = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'query', + name: {kind: 'Name', value: 'SchemaDefinitionByTarget'}, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: {kind: 'Variable', name: {kind: 'Name', value: 'handle'}}, + type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}}, + }, + { + kind: 'VariableDefinition', + variable: {kind: 'Variable', name: {kind: 'Name', value: 'version'}}, + type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}}, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: {kind: 'Name', value: 'target'}, + arguments: [ + { + kind: 'Argument', + name: {kind: 'Name', value: 'handle'}, + value: {kind: 'Variable', name: {kind: 'Name', value: 'handle'}}, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: {kind: 'Name', value: 'api'}, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: {kind: 'Name', value: 'schema'}, + arguments: [ + { + kind: 'Argument', + name: {kind: 'Name', value: 'version'}, + value: {kind: 'Variable', name: {kind: 'Name', value: 'version'}}, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + {kind: 'Field', name: {kind: 'Name', value: 'definition'}}, + {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, + ], + }, + }, + {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, + ], + }, + }, + {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode diff --git a/packages/app/src/cli/api/graphql/functions/generated/types.d.ts b/packages/app/src/cli/api/graphql/functions/generated/types.d.ts new file mode 100644 index 00000000000..2d85fa63eea --- /dev/null +++ b/packages/app/src/cli/api/graphql/functions/generated/types.d.ts @@ -0,0 +1,24 @@ +/* eslint-disable @typescript-eslint/consistent-type-definitions, @typescript-eslint/naming-convention */ +export type Maybe = T | null +export type InputMaybe = Maybe +export type Exact = {[K in keyof T]: T[K]} +export type MakeOptional = Omit & {[SubKey in K]?: Maybe} +export type MakeMaybe = Omit & {[SubKey in K]: Maybe} +export type MakeEmpty = {[_ in K]?: never} +export type Incremental = T | {[P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never} +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: {input: string; output: string} + String: {input: string; output: string} + Boolean: {input: boolean; output: boolean} + Int: {input: number; output: number} + Float: {input: number; output: number} + /** + * Represents an [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986) and + * [RFC 3987](https://datatracker.ietf.org/doc/html/rfc3987)-compliant URI string. + * + * For example, `"https://example.myshopify.com"` is a valid URL. It includes a scheme (`https`) and a host + * (`example.myshopify.com`). + */ + URL: {input: string; output: string} +} diff --git a/packages/app/src/cli/api/graphql/functions/queries/schema-definition-by-api-type.graphql b/packages/app/src/cli/api/graphql/functions/queries/schema-definition-by-api-type.graphql new file mode 100644 index 00000000000..c9b493ba7ce --- /dev/null +++ b/packages/app/src/cli/api/graphql/functions/queries/schema-definition-by-api-type.graphql @@ -0,0 +1,7 @@ +query SchemaDefinitionByApiType($type: String!, $version: String!) { + api(type: $type) { + schema(version: $version) { + definition + } + } +} diff --git a/packages/app/src/cli/api/graphql/functions/queries/schema-definition-by-target.graphql b/packages/app/src/cli/api/graphql/functions/queries/schema-definition-by-target.graphql new file mode 100644 index 00000000000..fd220b53fa3 --- /dev/null +++ b/packages/app/src/cli/api/graphql/functions/queries/schema-definition-by-target.graphql @@ -0,0 +1,9 @@ +query SchemaDefinitionByTarget($handle: String!, $version: String!) { + target(handle: $handle) { + api { + schema(version: $version) { + definition + } + } + } +} diff --git a/packages/app/src/cli/api/graphql/functions/target_schema_definition.ts b/packages/app/src/cli/api/graphql/functions/target_schema_definition.ts index dbf2ee2eacd..a957d03d65f 100644 --- a/packages/app/src/cli/api/graphql/functions/target_schema_definition.ts +++ b/packages/app/src/cli/api/graphql/functions/target_schema_definition.ts @@ -9,9 +9,3 @@ export const TargetSchemaDefinitionQuery = gql` export interface TargetSchemaDefinitionQuerySchema { definition: string | null } - -export interface TargetSchemaDefinitionQueryVariables { - apiKey: string - version: string - target: string -} diff --git a/packages/app/src/cli/commands/app/function/run.ts b/packages/app/src/cli/commands/app/function/run.ts index 92aa20063aa..5e4cd99db3e 100644 --- a/packages/app/src/cli/commands/app/function/run.ts +++ b/packages/app/src/cli/commands/app/function/run.ts @@ -41,7 +41,7 @@ export default class FunctionRun extends AppCommand { userProvidedConfigName: flags.config, apiKey: flags['client-id'], reset: flags.reset, - callback: async (app, developerPlatformClient, ourFunction) => { + callback: async (app, developerPlatformClient, ourFunction, orgId) => { let functionExport = DEFAULT_FUNCTION_EXPORT if (flags.export !== undefined) { @@ -77,7 +77,7 @@ export default class FunctionRun extends AppCommand { const inputQueryPath = ourFunction?.configuration.targeting?.[0]?.input_query const queryPath = inputQueryPath && `${ourFunction?.directory}/${inputQueryPath}` - const schemaPath = await getOrGenerateSchemaPath(ourFunction, app, developerPlatformClient) + const schemaPath = await getOrGenerateSchemaPath(ourFunction, app, developerPlatformClient, orgId) await runFunction({ functionExtension: ourFunction, diff --git a/packages/app/src/cli/commands/app/function/schema.ts b/packages/app/src/cli/commands/app/function/schema.ts index 199c94e572d..50819647eb5 100644 --- a/packages/app/src/cli/commands/app/function/schema.ts +++ b/packages/app/src/cli/commands/app/function/schema.ts @@ -46,13 +46,14 @@ export default class FetchSchema extends AppCommand { apiKey, reset: flags.reset, userProvidedConfigName: flags.config, - callback: async (app, developerPlatformClient, ourFunction) => { + callback: async (app, developerPlatformClient, ourFunction, orgId) => { await generateSchemaService({ app, extension: ourFunction, developerPlatformClient, stdout: flags.stdout, path: flags.path, + orgId, }) return app }, diff --git a/packages/app/src/cli/models/app/app.test-data.ts b/packages/app/src/cli/models/app/app.test-data.ts index b4fe7f3f61a..4cac2ecdbd9 100644 --- a/packages/app/src/cli/models/app/app.test-data.ts +++ b/packages/app/src/cli/models/app/app.test-data.ts @@ -55,8 +55,6 @@ import { } from '../../api/graphql/extension_migrate_flow_extension.js' import {UpdateURLsSchema, UpdateURLsVariables} from '../../api/graphql/update_urls.js' import {CurrentAccountInfoSchema} from '../../api/graphql/current_account_info.js' -import {TargetSchemaDefinitionQueryVariables} from '../../api/graphql/functions/target_schema_definition.js' -import {ApiSchemaDefinitionQueryVariables} from '../../api/graphql/functions/api_schema_definition.js' import { MigrateToUiExtensionSchema, MigrateToUiExtensionVariables, @@ -69,6 +67,8 @@ import { ExtensionUpdateDraftMutation, ExtensionUpdateDraftMutationVariables, } from '../../api/graphql/partners/generated/update-draft.js' +import {SchemaDefinitionByTargetQueryVariables} from '../../api/graphql/functions/generated/schema-definition-by-target.js' +import {SchemaDefinitionByApiTypeQueryVariables} from '../../api/graphql/functions/generated/schema-definition-by-api-type.js' import {vi} from 'vitest' import {joinPath} from '@shopify/cli-kit/node/path' @@ -1351,8 +1351,10 @@ export function testDeveloperPlatformClient(stubs: Partial Promise.resolve(migrateAppModuleResponse), updateURLs: (_input: UpdateURLsVariables) => Promise.resolve(updateURLsResponse), currentAccountInfo: () => Promise.resolve(currentAccountInfoResponse), - targetSchemaDefinition: (_input: TargetSchemaDefinitionQueryVariables) => Promise.resolve('schema'), - apiSchemaDefinition: (_input: ApiSchemaDefinitionQueryVariables) => Promise.resolve('schema'), + targetSchemaDefinition: (_input: SchemaDefinitionByTargetQueryVariables & {apiKey?: string}, _orgId: string) => + Promise.resolve('schema'), + apiSchemaDefinition: (_input: SchemaDefinitionByApiTypeQueryVariables & {apiKey?: string}, _orgId: string) => + Promise.resolve('schema'), migrateToUiExtension: (_input: MigrateToUiExtensionVariables) => Promise.resolve(migrateToUiExtensionResponse), toExtensionGraphQLType: (input: string) => input, subscribeToAppLogs: (_input: AppLogsSubscribeVariables) => Promise.resolve(appLogsSubscribeResponse), diff --git a/packages/app/src/cli/services/function/common.test.ts b/packages/app/src/cli/services/function/common.test.ts index 1fd0719823e..c338f1356eb 100644 --- a/packages/app/src/cli/services/function/common.test.ts +++ b/packages/app/src/cli/services/function/common.test.ts @@ -118,7 +118,7 @@ describe('getOrGenerateSchemaPath', () => { vi.mocked(fileExists).mockResolvedValue(true) // When - const result = await getOrGenerateSchemaPath(extension, app, developerPlatformClient) + const result = await getOrGenerateSchemaPath(extension, app, developerPlatformClient, '123') // Then expect(result).toBe(expectedPath) @@ -133,7 +133,7 @@ describe('getOrGenerateSchemaPath', () => { vi.mocked(fileExists).mockResolvedValueOnce(true) // When - const result = await getOrGenerateSchemaPath(extension, app, developerPlatformClient) + const result = await getOrGenerateSchemaPath(extension, app, developerPlatformClient, '123') // Then expect(result).toBe(expectedPath) diff --git a/packages/app/src/cli/services/function/common.ts b/packages/app/src/cli/services/function/common.ts index 638f3198197..98b0d812981 100644 --- a/packages/app/src/cli/services/function/common.ts +++ b/packages/app/src/cli/services/function/common.ts @@ -37,9 +37,10 @@ export async function inFunctionContext({ app: AppLinkedInterface, developerPlatformClient: DeveloperPlatformClient, ourFunction: ExtensionInstance, + orgId: string, ) => Promise }) { - const {app, developerPlatformClient} = await linkedAppContext({ + const {app, developerPlatformClient, organization} = await linkedAppContext({ directory: path, clientId: apiKey, forceRelink: reset ?? false, @@ -52,14 +53,14 @@ export async function inFunctionContext({ const ourFunction = allFunctions.find((fun) => fun.directory === path) if (ourFunction) { - return callback(app, developerPlatformClient, ourFunction) + return callback(app, developerPlatformClient, ourFunction, organization.id) } else if (isTerminalInteractive()) { const selectedFunction = await renderAutocompletePrompt({ message: 'Which function?', choices: allFunctions.map((shopifyFunction) => ({label: shopifyFunction.handle, value: shopifyFunction})), }) - return callback(app, developerPlatformClient, selectedFunction) + return callback(app, developerPlatformClient, selectedFunction, organization.id) } else { throw new AbortError('Run this command from a function directory or use `--path` to specify a function directory.') } @@ -69,6 +70,7 @@ export async function getOrGenerateSchemaPath( extension: ExtensionInstance, app: AppLinkedInterface, developerPlatformClient: DeveloperPlatformClient, + orgId: string, ): Promise { const path = joinPath(extension.directory, 'schema.graphql') if (await fileExists(path)) { @@ -81,6 +83,7 @@ export async function getOrGenerateSchemaPath( extension, stdout: false, path: extension.directory, + orgId, }) return (await fileExists(path)) ? path : undefined diff --git a/packages/app/src/cli/services/generate-schema.test.ts b/packages/app/src/cli/services/generate-schema.test.ts index 7a20343e8b5..1d034e47dab 100644 --- a/packages/app/src/cli/services/generate-schema.test.ts +++ b/packages/app/src/cli/services/generate-schema.test.ts @@ -1,9 +1,8 @@ import {generateSchemaService} from './generate-schema.js' import {testAppLinked, testDeveloperPlatformClient, testFunctionExtension} from '../models/app/app.test-data.js' -import {ApiSchemaDefinitionQueryVariables} from '../api/graphql/functions/api_schema_definition.js' import {describe, expect, vi, test} from 'vitest' import {AbortError} from '@shopify/cli-kit/node/error' -import {inTemporaryDirectory, readFile} from '@shopify/cli-kit/node/fs' +import {inTemporaryDirectory, readFile, mkdir} from '@shopify/cli-kit/node/fs' import {joinPath} from '@shopify/cli-kit/node/path' import * as output from '@shopify/cli-kit/node/output' @@ -28,22 +27,27 @@ describe('generateSchemaService', () => { test('Save the latest GraphQL schema to ./[extension]/schema.graphql when stdout flag is ABSENT', async () => { await inTemporaryDirectory(async (tmpDir) => { // Given + const orgId = 'test' + const extensionDir = joinPath(tmpDir, 'extensions', 'my-function') + await mkdir(extensionDir) + const app = testAppLinked() - const extension = await testFunctionExtension({}) - const apiKey = 'api-key' - const path = tmpDir + const extension = await testFunctionExtension({ + dir: tmpDir, + }) // When await generateSchemaService({ app, extension, - path, + path: tmpDir, stdout: false, developerPlatformClient: testDeveloperPlatformClient(), + orgId, }) // Then - const outputFile = await readFile(joinPath(tmpDir, 'schema.graphql')) + const outputFile = await readFile(joinPath(extension.directory, 'schema.graphql')) expect(outputFile).toEqual('schema') }) }) @@ -55,6 +59,7 @@ describe('generateSchemaService', () => { const extension = await testFunctionExtension() const path = tmpDir const stdout = true + const orgId = '123' const mockOutput = vi.fn() vi.spyOn(output, 'outputInfo').mockImplementation(mockOutput) @@ -65,6 +70,7 @@ describe('generateSchemaService', () => { path, stdout, developerPlatformClient: testDeveloperPlatformClient(), + orgId, }) // Then @@ -75,9 +81,12 @@ describe('generateSchemaService', () => { describe('GraphQL query', () => { test('Uses ApiSchemaDefinitionQuery when not using targets', async () => { await inTemporaryDirectory(async (tmpDir) => { - // Given + const extensionDir = joinPath(tmpDir, 'extensions', 'my-function') + await mkdir(extensionDir) + const app = testAppLinked() const extension = await testFunctionExtension({ + dir: tmpDir, config: { name: 'test function extension', description: 'description', @@ -90,37 +99,41 @@ describe('generateSchemaService', () => { metafields: [], }, }) - const apiKey = 'api-key' + + const orgId = 'test' const path = tmpDir - const { - configuration: {api_version: version}, - type, - } = extension + const version = extension.configuration.api_version const developerPlatformClient = testDeveloperPlatformClient() - // When await generateSchemaService({ app, extension, path, stdout: false, developerPlatformClient, + orgId, }) - // Then - expect(developerPlatformClient.apiSchemaDefinition).toHaveBeenCalledWith({ - apiKey, - version, - type, - }) + expect(developerPlatformClient.apiSchemaDefinition).toHaveBeenCalledWith( + { + version, + type: extension.configuration.type, + }, + app.configuration.client_id, + orgId, + app.configuration.app_id, + ) }) }) test('Uses TargetSchemaDefinitionQuery when targets present', async () => { await inTemporaryDirectory(async (tmpDir) => { - // Given + const extensionDir = joinPath(tmpDir, 'extensions', 'my-function') + await mkdir(extensionDir) + const app = testAppLinked() const extension = await testFunctionExtension({ + dir: tmpDir, config: { name: 'test function extension', description: 'description', @@ -141,27 +154,31 @@ describe('generateSchemaService', () => { metafields: [], }, }) - const apiKey = 'api-key' + const path = tmpDir const expectedTarget = extension.configuration.targeting![0]!.target const version = extension.configuration.api_version + const orgId = 'test' const developerPlatformClient = testDeveloperPlatformClient() - // When await generateSchemaService({ app, extension, path, stdout: false, developerPlatformClient, + orgId, }) - // Then - expect(developerPlatformClient.targetSchemaDefinition).toHaveBeenCalledWith({ - apiKey, - version, - target: expectedTarget, - }) + expect(developerPlatformClient.targetSchemaDefinition).toHaveBeenCalledWith( + { + handle: expectedTarget, + version, + }, + app.configuration.client_id, + orgId, + app.configuration.app_id, + ) }) }) }) @@ -170,9 +187,9 @@ describe('generateSchemaService', () => { // Given const app = testAppLinked() const extension = await testFunctionExtension() - const apiKey = 'api-key' + const orgId = '123' const developerPlatformClient = testDeveloperPlatformClient({ - apiSchemaDefinition: (_input: ApiSchemaDefinitionQueryVariables) => Promise.resolve(null), + apiSchemaDefinition: () => Promise.resolve(null), }) // When @@ -182,6 +199,7 @@ describe('generateSchemaService', () => { path: '', stdout: true, developerPlatformClient, + orgId, }) // Then diff --git a/packages/app/src/cli/services/generate-schema.ts b/packages/app/src/cli/services/generate-schema.ts index 8544d50b5b2..a0d544ff8aa 100644 --- a/packages/app/src/cli/services/generate-schema.ts +++ b/packages/app/src/cli/services/generate-schema.ts @@ -1,6 +1,6 @@ import {DeveloperPlatformClient} from '../utilities/developer-platform-client.js' -import {ApiSchemaDefinitionQueryVariables} from '../api/graphql/functions/api_schema_definition.js' -import {TargetSchemaDefinitionQueryVariables} from '../api/graphql/functions/target_schema_definition.js' +import {SchemaDefinitionByApiTypeQueryVariables} from '../api/graphql/functions/generated/schema-definition-by-api-type.js' +import {SchemaDefinitionByTargetQueryVariables} from '../api/graphql/functions/generated/schema-definition-by-target.js' import {ExtensionInstance} from '../models/extensions/extension-instance.js' import {FunctionConfigType} from '../models/extensions/specifications/function.js' import {AppLinkedInterface} from '../models/app/app.js' @@ -15,12 +15,13 @@ interface GenerateSchemaOptions { stdout: boolean path: string developerPlatformClient: DeveloperPlatformClient + orgId: string } export async function generateSchemaService(options: GenerateSchemaOptions) { - const {extension, stdout, developerPlatformClient, app} = options + const {extension, stdout, developerPlatformClient, app, orgId} = options const apiKey = app.configuration.client_id - + const appId = app.configuration.app_id const {api_version: version, type, targeting} = extension.configuration const usingTargets = Boolean(targeting?.length) const definition = await (usingTargets @@ -28,22 +29,26 @@ export async function generateSchemaService(options: GenerateSchemaOptions) { localIdentifier: extension.localIdentifier, developerPlatformClient, apiKey, + appId, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion target: targeting![0]!.target, version, + orgId, }) - : generateSchemaFromType({ + : generateSchemaFromApiType({ localIdentifier: extension.localIdentifier, developerPlatformClient, apiKey, + appId, type, version, + orgId, })) if (stdout) { outputInfo(definition) } else { - const outputPath = joinPath(options.path, 'schema.graphql') + const outputPath = joinPath(extension.directory, 'schema.graphql') await writeFile(outputPath, definition) outputInfo(`GraphQL Schema for ${extension.localIdentifier} written to ${outputPath}`) } @@ -53,7 +58,9 @@ interface BaseGenerateSchemaOptions { localIdentifier: string developerPlatformClient: DeveloperPlatformClient apiKey: string + appId?: string version: string + orgId: string } interface GenerateSchemaFromTargetOptions extends BaseGenerateSchemaOptions { @@ -63,16 +70,18 @@ interface GenerateSchemaFromTargetOptions extends BaseGenerateSchemaOptions { async function generateSchemaFromTarget({ localIdentifier, developerPlatformClient, + appId, apiKey, target, version, + orgId, }: GenerateSchemaFromTargetOptions): Promise { - const variables: TargetSchemaDefinitionQueryVariables = { - apiKey, - target, + const variables: SchemaDefinitionByTargetQueryVariables = { + handle: target, version, } - const definition = await developerPlatformClient.targetSchemaDefinition(variables) + // Api key required for partners reqs, can be removed once fully migrated to AMF + const definition = await developerPlatformClient.targetSchemaDefinition(variables, apiKey, orgId, appId) if (!definition) { throw new AbortError( @@ -88,19 +97,21 @@ interface GenerateSchemaFromType extends BaseGenerateSchemaOptions { type: string } -async function generateSchemaFromType({ +async function generateSchemaFromApiType({ localIdentifier, developerPlatformClient, apiKey, + appId, version, type, + orgId, }: GenerateSchemaFromType): Promise { - const variables: ApiSchemaDefinitionQueryVariables = { - apiKey, + const variables: SchemaDefinitionByApiTypeQueryVariables = { version, type, } - const definition = await developerPlatformClient.apiSchemaDefinition(variables) + + const definition = await developerPlatformClient.apiSchemaDefinition(variables, apiKey, orgId, appId) if (!definition) { throw new AbortError( diff --git a/packages/app/src/cli/utilities/developer-platform-client.ts b/packages/app/src/cli/utilities/developer-platform-client.ts index e630f926f46..e1eb8f1c973 100644 --- a/packages/app/src/cli/utilities/developer-platform-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client.ts @@ -36,8 +36,8 @@ import { import {UpdateURLsSchema, UpdateURLsVariables} from '../api/graphql/update_urls.js' import {CurrentAccountInfoSchema} from '../api/graphql/current_account_info.js' import {ExtensionTemplate} from '../models/app/template.js' -import {TargetSchemaDefinitionQueryVariables} from '../api/graphql/functions/target_schema_definition.js' -import {ApiSchemaDefinitionQueryVariables} from '../api/graphql/functions/api_schema_definition.js' +import {SchemaDefinitionByTargetQueryVariables} from '../api/graphql/functions/generated/schema-definition-by-target.js' +import {SchemaDefinitionByApiTypeQueryVariables} from '../api/graphql/functions/generated/schema-definition-by-api-type.js' import { MigrateToUiExtensionSchema, MigrateToUiExtensionVariables, @@ -248,8 +248,18 @@ export interface DeveloperPlatformClient { migrateAppModule: (input: MigrateAppModuleVariables) => Promise updateURLs: (input: UpdateURLsVariables) => Promise currentAccountInfo: () => Promise - targetSchemaDefinition: (input: TargetSchemaDefinitionQueryVariables) => Promise - apiSchemaDefinition: (input: ApiSchemaDefinitionQueryVariables) => Promise + targetSchemaDefinition: ( + input: SchemaDefinitionByTargetQueryVariables, + apiKey: string, + organizationId: string, + appId?: string, + ) => Promise + apiSchemaDefinition: ( + input: SchemaDefinitionByApiTypeQueryVariables, + apiKey: string, + organizationId: string, + appId?: string, + ) => Promise migrateToUiExtension: (input: MigrateToUiExtensionVariables) => Promise toExtensionGraphQLType: (input: string) => string subscribeToAppLogs: (input: AppLogsSubscribeVariables) => Promise diff --git a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts index 098849f3fe7..03b1efaca3b 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts @@ -60,8 +60,6 @@ import { import {UpdateURLsSchema, UpdateURLsVariables} from '../../api/graphql/update_urls.js' import {CurrentAccountInfoSchema} from '../../api/graphql/current_account_info.js' import {ExtensionTemplate} from '../../models/app/template.js' -import {TargetSchemaDefinitionQueryVariables} from '../../api/graphql/functions/target_schema_definition.js' -import {ApiSchemaDefinitionQueryVariables} from '../../api/graphql/functions/api_schema_definition.js' import { MigrateToUiExtensionVariables, MigrateToUiExtensionSchema, @@ -107,6 +105,16 @@ import {UserInfo} from '../../api/graphql/business-platform-destinations/generat import {AvailableTopics} from '../../api/graphql/webhooks/generated/available-topics.js' import {CliTesting} from '../../api/graphql/webhooks/generated/cli-testing.js' import {PublicApiVersions} from '../../api/graphql/webhooks/generated/public-api-versions.js' +import { + SchemaDefinitionByTarget, + SchemaDefinitionByTargetQuery, + SchemaDefinitionByTargetQueryVariables, +} from '../../api/graphql/functions/generated/schema-definition-by-target.js' +import { + SchemaDefinitionByApiType, + SchemaDefinitionByApiTypeQuery, + SchemaDefinitionByApiTypeQueryVariables, +} from '../../api/graphql/functions/generated/schema-definition-by-api-type.js' import {ensureAuthenticatedAppManagement, ensureAuthenticatedBusinessPlatform} from '@shopify/cli-kit/node/session' import {isUnitTest} from '@shopify/cli-kit/node/context/local' import {AbortError, BugError} from '@shopify/cli-kit/node/error' @@ -123,6 +131,7 @@ import {versionSatisfies} from '@shopify/cli-kit/node/node-package-manager' import {outputDebug} from '@shopify/cli-kit/node/output' import {developerDashboardFqdn} from '@shopify/cli-kit/node/context/fqdn' import {webhooksRequest} from '@shopify/cli-kit/node/api/webhooks' +import {functionsRequestDoc} from '@shopify/cli-kit/node/api/functions' const TEMPLATE_JSON_URL = 'https://cdn.shopify.com/static/cli/extensions/templates.json' @@ -778,12 +787,53 @@ export class AppManagementClient implements DeveloperPlatformClient { throw new BugError('Not implemented: currentAccountInfo') } - async targetSchemaDefinition(_input: TargetSchemaDefinitionQueryVariables): Promise { - throw new BugError('Not implemented: targetSchemaDefinition') + async targetSchemaDefinition( + input: SchemaDefinitionByTargetQueryVariables, + _apiKey: string, + organizationId: string, + appId?: string, + ): Promise { + try { + const appIdNumber = String(numberFromGid(appId!)) + const token = await this.token() + const result = await functionsRequestDoc( + organizationId, + SchemaDefinitionByTarget, + token, + appIdNumber, + { + handle: input.handle, + version: input.version, + }, + ) + + return result?.target?.api?.schema?.definition ?? null + } catch (error) { + throw new AbortError(`Failed to fetch schema definition: ${error}`) + } } - async apiSchemaDefinition(_input: ApiSchemaDefinitionQueryVariables): Promise { - throw new BugError('Not implemented: apiSchemaDefinition') + async apiSchemaDefinition( + input: SchemaDefinitionByApiTypeQueryVariables, + _apiKey: string, + organizationId: string, + appId?: string, + ): Promise { + try { + const appIdNumber = String(numberFromGid(appId!)) + const token = await this.token() + const result = await functionsRequestDoc( + organizationId, + SchemaDefinitionByApiType, + token, + appIdNumber, + input, + ) + + return result?.api?.schema?.definition ?? null + } catch (error) { + throw new AbortError(`Failed to fetch schema definition: ${error}`) + } } async migrateToUiExtension(_input: MigrateToUiExtensionVariables): Promise { diff --git a/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts b/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts index 0db97d7b026..72230b2e295 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts @@ -99,12 +99,10 @@ import { } from '../../api/graphql/template_specifications.js' import {ExtensionTemplate} from '../../models/app/template.js' import { - TargetSchemaDefinitionQueryVariables, TargetSchemaDefinitionQuerySchema, TargetSchemaDefinitionQuery, } from '../../api/graphql/functions/target_schema_definition.js' import { - ApiSchemaDefinitionQueryVariables, ApiSchemaDefinitionQuerySchema, ApiSchemaDefinitionQuery, } from '../../api/graphql/functions/api_schema_definition.js' @@ -153,6 +151,8 @@ import { DevStoresByOrgQuery, DevStoresByOrgQueryVariables, } from '../../api/graphql/partners/generated/dev-stores-by-org.js' +import {SchemaDefinitionByTargetQueryVariables} from '../../api/graphql/functions/generated/schema-definition-by-target.js' +import {SchemaDefinitionByApiTypeQueryVariables} from '../../api/graphql/functions/generated/schema-definition-by-api-type.js' import {TypedDocumentNode} from '@graphql-typed-document-node/core' import {isUnitTest} from '@shopify/cli-kit/node/context/local' import {AbortError} from '@shopify/cli-kit/node/error' @@ -505,13 +505,37 @@ export class PartnersClient implements DeveloperPlatformClient { return this.request(CurrentAccountInfoQuery) } - async targetSchemaDefinition(input: TargetSchemaDefinitionQueryVariables): Promise { - const response: TargetSchemaDefinitionQuerySchema = await this.request(TargetSchemaDefinitionQuery, input) + async targetSchemaDefinition( + input: SchemaDefinitionByTargetQueryVariables, + apiKey: string, + _organizationId: string, + _appId?: string, + ): Promise { + // Ensures compatibility with existing partners requests + // Can remove once migrated to AMF + const transformedInput = { + target: input.handle, + version: input.version, + apiKey, + } + + const response: TargetSchemaDefinitionQuerySchema = await this.request( + TargetSchemaDefinitionQuery, + transformedInput, + ) return response.definition } - async apiSchemaDefinition(input: ApiSchemaDefinitionQueryVariables): Promise { - const response: ApiSchemaDefinitionQuerySchema = await this.request(ApiSchemaDefinitionQuery, input) + async apiSchemaDefinition( + input: SchemaDefinitionByApiTypeQueryVariables & {apiKey?: string}, + apiKey: string, + _organizationId: string, + _appId?: string, + ): Promise { + const response: ApiSchemaDefinitionQuerySchema = await this.request(ApiSchemaDefinitionQuery, { + ...input, + apiKey, + }) return response.definition } diff --git a/packages/cli-kit/src/public/node/api/functions.ts b/packages/cli-kit/src/public/node/api/functions.ts new file mode 100644 index 00000000000..65c07fc766c --- /dev/null +++ b/packages/cli-kit/src/public/node/api/functions.ts @@ -0,0 +1,63 @@ +import {graphqlRequestDoc} from './graphql.js' +import {handleDeprecations} from './app-management.js' +import {appManagementFqdn} from '../context/fqdn.js' +import {TypedDocumentNode} from '@graphql-typed-document-node/core' +import {Variables} from 'graphql-request' +import Bottleneck from 'bottleneck' + +// API Rate limiter for partners API (Limit is 10 requests per second) +// Jobs are launched every 150ms to add an extra 50ms margin per request. +// Only 10 requests can be executed concurrently. +const limiter = new Bottleneck({ + minTime: 150, + maxConcurrent: 10, +}) + +/** + * Prepares the request configuration for the App Management Functions API. + * + * @param orgId - Organization identifier. + * @param token - Authentication token. + * @param appId - App identifier. + * @returns Request configuration object. + */ +async function setupRequest(orgId: string, token: string, appId: string) { + const api = 'Functions' + const fqdn = await appManagementFqdn() + const url = `https://${fqdn}/functions/unstable/organizations/${orgId}/${appId}/graphql` + + return { + token, + api, + url, + responseOptions: {onResponse: handleDeprecations}, + } +} + +/** + * Executes a rate-limited GraphQL request against the App Management Functions API. + * + * @param orgId - Organization identifier. + * @param query - Typed GraphQL document node. + * @param token - Authentication token. + * @param appId - App identifier. + * @param variables - Optional query variables. + * @returns Promise resolving to the typed query result. + */ +export async function functionsRequestDoc( + orgId: string, + query: TypedDocumentNode, + token: string, + appId: string, + variables?: TVariables, +): Promise { + const result = await limiter.schedule(async () => { + return graphqlRequestDoc({ + ...(await setupRequest(orgId, token, appId)), + query, + variables, + }) + }) + + return result +} diff --git a/packages/eslint-plugin-cli/rules/no-inline-graphql.js b/packages/eslint-plugin-cli/rules/no-inline-graphql.js index 0c3e5991a33..9287a1ca65d 100644 --- a/packages/eslint-plugin-cli/rules/no-inline-graphql.js +++ b/packages/eslint-plugin-cli/rules/no-inline-graphql.js @@ -152,10 +152,10 @@ const knownFailures = { '867f01113c20386d6a438dd56a6d241199e407eab928ab1ad9a7f233cd35c1be', 'packages/app/src/cli/api/graphql/find_store_by_domain.ts': '0824f5baaab1ad419a7fa1d64824e306bd369430da47c7457ed72e74a1e94a9a', - 'packages/app/src/cli/api/graphql/functions/api_schema_definition.ts': - '9ebbab831a66a2e49a8c2dac3185bb58c4688655040c6d14a9b2fb41a004bca8', + 'packages/app/src/cli/api/graphql/functions/api_schema_definition.ts': + 'e71100cf61919831681da1be8f12cd9067c4e3f2faf04c1b88b764fd8a275b82', 'packages/app/src/cli/api/graphql/functions/target_schema_definition.ts': - '0469c1fe724568f031a52c5bda54d56d8c3b23d7e8a54a86a684f72028d71b46', + 'd338c5d187ca8a1e1e68892987d780e540426faeba89df2dd9d8c96e193f5c13', 'packages/app/src/cli/api/graphql/generate_signed_upload_url.ts': '848e40bf6b44331de0fe1dc1b0753593c1d47f9705ebe988a1b8ad5638d267ef', 'packages/app/src/cli/api/graphql/get_variant_id.ts':