diff --git a/package.json b/package.json index ba439a7f6..e5f77a319 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "release": "pnpm build:libraries && changeset publish", "release:docs:update-version": "tsx scripts/sync-docker-image-tag-docs.ts", "release:version": "changeset version && pnpm --filter hive-apollo-router-plugin sync-cargo-file && pnpm build:libraries && pnpm --filter @graphql-hive/cli oclif:readme && pnpm run release:docs:update-version", + "seed:app-deployments": "tsx scripts/seed-app-deployments.mts", "seed:org": "tsx scripts/seed-organization.mts", "seed:schemas": "tsx scripts/seed-schemas.ts", "seed:usage": "tsx scripts/seed-usage.ts", diff --git a/scripts/seed-app-deployments.mts b/scripts/seed-app-deployments.mts new file mode 100644 index 000000000..0f681e2ea --- /dev/null +++ b/scripts/seed-app-deployments.mts @@ -0,0 +1,283 @@ +/** + * Script for seeding app deployments into an existing target. + * + * Requirements: + * - Docker Compose is started (pnpm start) + * - FEATURE_FLAGS_APP_DEPLOYMENTS_ENABLED=1 in server .env + * - A published schema in the target + * + * Example: + * `TOKEN= pnpm seed:app-deployments` + * + * Where is a Target Access Token from the target's Settings page. + */ + +const token = process.env.TOKEN || process.env.HIVE_TOKEN; + +if (!token) { + console.error('Missing "TOKEN" environment variable.'); + console.error('Usage: TOKEN= pnpm seed:app-deployments'); + process.exit(1); +} + +const graphqlEndpoint = 'http://localhost:3001/graphql'; + +async function executeGraphQL(query: string, variables: Record): Promise { + const response = await fetch(graphqlEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ query, variables }), + }); + + const result = (await response.json()) as { data?: T; errors?: Array<{ message: string }> }; + + if (result.errors?.length) { + throw new Error(`GraphQL Error: ${result.errors.map(e => e.message).join(', ')}`); + } + + return result.data as T; +} + +const CreateAppDeployment = /* GraphQL */ ` + mutation CreateAppDeployment($input: CreateAppDeploymentInput!) { + createAppDeployment(input: $input) { + error { + message + } + ok { + createdAppDeployment { + id + name + version + status + } + } + } + } +`; + +const AddDocumentsToAppDeployment = /* GraphQL */ ` + mutation AddDocumentsToAppDeployment($input: AddDocumentsToAppDeploymentInput!) { + addDocumentsToAppDeployment(input: $input) { + error { + message + } + ok { + appDeployment { + id + name + version + status + } + } + } + } +`; + +const ActivateAppDeployment = /* GraphQL */ ` + mutation ActivateAppDeployment($input: ActivateAppDeploymentInput!) { + activateAppDeployment(input: $input) { + error { + message + } + ok { + activatedAppDeployment { + id + name + version + status + } + } + } + } +`; + +const RetireAppDeployment = /* GraphQL */ ` + mutation RetireAppDeployment($input: RetireAppDeploymentInput!) { + retireAppDeployment(input: $input) { + error { + message + } + ok { + retiredAppDeployment { + id + name + version + status + } + } + } + } +`; + +// Sample GraphQL documents for app deployments +const sampleDocuments = [ + { hash: 'get-user-query', body: 'query GetUser($id: ID!) { user(id: $id) { id name email } }' }, + { + hash: 'list-users-query', + body: 'query ListUsers($limit: Int) { users(limit: $limit) { id name } }', + }, + { + hash: 'create-user-mutation', + body: 'mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id name } }', + }, + { + hash: 'update-user-mutation', + body: 'mutation UpdateUser($id: ID!, $input: UpdateUserInput!) { updateUser(id: $id, input: $input) { id name } }', + }, + { hash: 'delete-user-mutation', body: 'mutation DeleteUser($id: ID!) { deleteUser(id: $id) }' }, + { + hash: 'get-products-query', + body: 'query GetProducts($category: String) { products(category: $category) { id name price } }', + }, + { + hash: 'get-product-query', + body: 'query GetProduct($id: ID!) { product(id: $id) { id name price description } }', + }, + { + hash: 'search-query', + body: 'query Search($term: String!) { search(term: $term) { __typename ... on User { id name } ... on Product { id name } } }', + }, +]; + +// App deployment configurations to create +const appDeployments = [ + { name: 'web-app', versions: ['1.0.0', '1.1.0', '1.2.0', '2.0.0'] }, + { name: 'mobile-app', versions: ['3.0.0', '3.1.0', '3.2.0'] }, + { name: 'admin-dashboard', versions: ['1.0.0', '1.0.1'] }, + { name: 'cli-tool', versions: ['0.1.0', '0.2.0', '1.0.0'] }, +]; + +async function createAppDeploymentWithDocuments( + appName: string, + appVersion: string, + documents: Array<{ hash: string; body: string }>, + shouldActivate: boolean, + shouldRetire: boolean = false, +) { + // Create the app deployment + const createResult = await executeGraphQL<{ + createAppDeployment: { + error: { message: string } | null; + ok: { + createdAppDeployment: { id: string; name: string; version: string; status: string }; + } | null; + }; + }>(CreateAppDeployment, { input: { appName, appVersion } }); + + if (createResult.createAppDeployment.error) { + console.error( + ` Failed to create ${appName}@${appVersion}:`, + createResult.createAppDeployment.error.message, + ); + return null; + } + + console.log(` Created ${appName}@${appVersion} (pending)`); + + // Add documents + const addDocsResult = await executeGraphQL<{ + addDocumentsToAppDeployment: { + error: { message: string } | null; + ok: { appDeployment: { id: string } } | null; + }; + }>(AddDocumentsToAppDeployment, { input: { appName, appVersion, documents } }); + + if (addDocsResult.addDocumentsToAppDeployment.error) { + console.error( + ` Failed to add documents to ${appName}@${appVersion}:`, + addDocsResult.addDocumentsToAppDeployment.error.message, + ); + return null; + } + + console.log(` Added ${documents.length} documents to ${appName}@${appVersion}`); + + if (shouldActivate) { + const activateResult = await executeGraphQL<{ + activateAppDeployment: { + error: { message: string } | null; + ok: { activatedAppDeployment: { id: string } } | null; + }; + }>(ActivateAppDeployment, { input: { appName, appVersion } }); + + if (activateResult.activateAppDeployment.error) { + console.error( + ` Failed to activate ${appName}@${appVersion}:`, + activateResult.activateAppDeployment.error.message, + ); + return null; + } + + console.log(` Activated ${appName}@${appVersion}`); + + if (shouldRetire) { + const retireResult = await executeGraphQL<{ + retireAppDeployment: { + error: { message: string } | null; + ok: { retiredAppDeployment: { id: string } } | null; + }; + }>(RetireAppDeployment, { input: { appName, appVersion } }); + + if (retireResult.retireAppDeployment.error) { + console.error( + ` Failed to retire ${appName}@${appVersion}:`, + retireResult.retireAppDeployment.error.message, + ); + return null; + } + + console.log(` Retired ${appName}@${appVersion}`); + } + } + + return createResult.createAppDeployment.ok?.createdAppDeployment; +} + +console.log(` + GraphQL endpoint: ${graphqlEndpoint} +`); + +console.log('Creating app deployments...\n'); + +for (const app of appDeployments) { + console.log(`Creating deployments for ${app.name}:`); + + for (let i = 0; i < app.versions.length; i++) { + const version = app.versions[i]; + const isLatest = i === app.versions.length - 1; + const isOldest = i === 0; + + // Select a subset of documents for variety + const docsToUse = sampleDocuments.slice(0, 3 + (i % 5)); + + // Activate all versions, retire old ones (except the latest) + const shouldActivate = true; + const shouldRetire = !isLatest && isOldest; + + await createAppDeploymentWithDocuments( + app.name, + version, + docsToUse, + shouldActivate, + shouldRetire, + ); + } + console.log(''); +} + +// Create one pending deployment (not activated) +console.log('Creating a pending deployment:'); +await createAppDeploymentWithDocuments( + 'beta-app', + '0.0.1-beta', + sampleDocuments.slice(0, 2), + false, // Don't activate +); + +console.log('\n========================================'); +console.log('App deployments seeded successfully!'); +console.log('========================================\n');