import { randomUUID } from 'node:crypto'; import { writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { execaCommand } from '@esm2cjs/execa'; import { fetchLatestSchema, fetchLatestValidSchema } from './flow'; import { getServiceHost } from './utils'; const binPath = resolve(__dirname, '../../packages/libraries/cli/bin/run'); const cliDir = resolve(__dirname, '../../packages/libraries/cli'); async function generateTmpFile(content: string, extension: string) { const dir = tmpdir(); const fileName = randomUUID(); const filepath = join(dir, `${fileName}.${extension}`); await writeFile(filepath, content, 'utf-8'); return filepath; } async function exec(cmd: string) { const outout = await execaCommand(`${binPath} ${cmd}`, { shell: true, env: { OCLIF_CLI_CUSTOM_PATH: cliDir, NODE_OPTIONS: '--no-deprecation', }, }); if (outout.failed) { throw new Error(outout.stderr); } return outout.stdout; } export async function schemaPublish(args: string[]) { const registryAddress = await getServiceHost('server', 8082); return await exec( ['schema:publish', `--registry.endpoint`, `http://${registryAddress}/graphql`, ...args].join( ' ', ), ); } export async function schemaCheck(args: string[]) { const registryAddress = await getServiceHost('server', 8082); return await exec( ['schema:check', `--registry.endpoint`, `http://${registryAddress}/graphql`, ...args].join(' '), ); } export async function schemaDelete(args: string[]) { const registryAddress = await getServiceHost('server', 8082); return await exec( ['schema:delete', `--registry.endpoint`, `http://${registryAddress}/graphql`, ...args].join( ' ', ), ); } async function dev(args: string[]) { const registryAddress = await getServiceHost('server', 8082); return await exec( ['dev', `--registry.endpoint`, `http://${registryAddress}/graphql`, ...args].join(' '), ); } export function createCLI(tokens: { readwrite: string; readonly: string }) { let publishCount = 0; async function publish({ sdl, serviceName, serviceUrl, metadata, expect: expectedStatus, legacy_force, legacy_acceptBreakingChanges, }: { sdl: string; commit?: string; serviceName?: string; serviceUrl?: string; metadata?: Record; legacy_force?: boolean; legacy_acceptBreakingChanges?: boolean; expect: 'latest' | 'latest-composable' | 'ignored' | 'rejected'; }): Promise { const publishName = ` #${++publishCount}`; const commit = randomUUID(); const cmd = schemaPublish([ '--registry.accessToken', tokens.readwrite, '--author', 'Kamil', '--commit', commit, ...(serviceName ? ['--service', serviceName] : []), ...(serviceUrl ? ['--url', serviceUrl] : []), ...(metadata ? ['--metadata', await generateTmpFile(JSON.stringify(metadata), 'json')] : []), ...(legacy_force ? ['--force'] : []), ...(legacy_acceptBreakingChanges ? ['--experimental_acceptBreakingChanges'] : []), await generateTmpFile(sdl, 'graphql'), ]); const expectedCommit = expect.objectContaining({ commit, }); if (expectedStatus === 'rejected') { await expect(cmd).rejects.toThrow(); const latestSchemaResult = await fetchLatestSchema(tokens.readonly).then(r => r.expectNoGraphQLErrors(), ); const latestSchemaCommit = latestSchemaResult.latestVersion?.log; expect( latestSchemaCommit, `${publishName} was expected to be rejected but was published`, ).not.toEqual(expectedCommit); return ''; } // throw if the command fails await cmd; const latestSchemaResult = await fetchLatestSchema(tokens.readonly).then(r => r.expectNoGraphQLErrors(), ); const latestSchemaCommit = latestSchemaResult.latestVersion?.log; if (expectedStatus === 'ignored') { // Check if the schema was ignored expect( latestSchemaCommit, `${publishName} was expected to be ignored but it was published`, ).not.toEqual(expectedCommit); return ''; } // Check if the schema was published expect(latestSchemaCommit, `${publishName} was expected to be published`).toEqual( expectedCommit, ); const latestComposableSchemaResult = await fetchLatestValidSchema(tokens.readonly).then(r => r.expectNoGraphQLErrors(), ); const latestComposableSchemaCommit = latestComposableSchemaResult.latestValidVersion?.log; // Check if the schema was published as composable or non-composable if (expectedStatus === 'latest') { // schema is not available to the gateway expect( latestComposableSchemaCommit, `${publishName} was expected to be published but not as composable`, ).not.toEqual(expectedCommit); } else { // schema is available to the gateway expect( latestComposableSchemaCommit, `${publishName} was expected to be published as composable`, ).toEqual(expectedCommit); } return await cmd; } async function check({ sdl, serviceName, expect: expectedStatus, }: { sdl: string; serviceName?: string; expect: 'approved' | 'rejected'; }): Promise { const cmd = schemaCheck([ '--registry.accessToken', tokens.readonly, ...(serviceName ? ['--service', serviceName] : []), await generateTmpFile(sdl, 'graphql'), ]); if (expectedStatus === 'rejected') { await expect(cmd).rejects.toThrow(); return cmd.catch(reason => Promise.resolve(reason.message)); } return cmd; } async function deleteCommand({ serviceName, expect: expectedStatus, }: { serviceName?: string; expect: 'latest' | 'latest-composable' | 'rejected'; }): Promise { const cmd = schemaDelete(['--token', tokens.readwrite, '--confirm', serviceName ?? '']); const before = { latest: await fetchLatestSchema(tokens.readonly).then(r => r.expectNoGraphQLErrors()), latestValid: await fetchLatestValidSchema(tokens.readonly).then(r => r.expectNoGraphQLErrors(), ), }; if (expectedStatus === 'rejected') { await expect(cmd).rejects.toThrow(); const after = { latest: await fetchLatestSchema(tokens.readonly).then(r => r.expectNoGraphQLErrors()), latestValid: await fetchLatestValidSchema(tokens.readonly).then(r => r.expectNoGraphQLErrors(), ), }; expect(after.latest.latestVersion?.log).toEqual(before.latest.latestVersion?.log); expect(after.latestValid.latestValidVersion?.id).toEqual( before.latestValid.latestValidVersion?.id, ); return cmd.catch(reason => Promise.resolve(reason.message)); } await cmd; const after = { latest: await fetchLatestSchema(tokens.readonly).then(r => r.expectNoGraphQLErrors()), latestValid: await fetchLatestValidSchema(tokens.readonly).then(r => r.expectNoGraphQLErrors(), ), }; if (expectedStatus === 'latest-composable') { expect(after.latest.latestVersion?.log).not.toEqual(before.latest.latestVersion?.log); expect(after.latestValid.latestValidVersion?.log).not.toEqual( before.latestValid.latestValidVersion?.log, ); } else { expect(after.latest.latestVersion?.log).not.toEqual(before.latest.latestVersion?.log); expect(after.latestValid.latestValidVersion?.id).toEqual( before.latestValid.latestValidVersion?.id, ); } return cmd; } async function devCmd(input: { services: Array<{ name: string; url: string; sdl: string; }>; remote: boolean; write?: string; useLatestVersion?: boolean; }) { return dev([ ...(input.remote ? [ '--remote', '--registry.accessToken', tokens.readonly, input.useLatestVersion ? '--unstable__forceLatest' : '', ] : []), input.write ? `--write ${input.write}` : '', ...(await Promise.all( input.services.map(async ({ name, url, sdl }) => { return [ '--service', name, '--url', url, '--schema', await generateTmpFile(sdl, 'graphql'), ].join(' '); }), )), ]); } return { publish, check, delete: deleteCommand, dev: devCmd, }; }