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

feat(@graphql-hive/cli): json output #6122

Draft
wants to merge 129 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
129 commits
Select commit Hold shift + click to select a range
7dc21e7
feat(@graphql-hive/cli): json flag for json output
jasonkuhrt Dec 12, 2024
de6c0dc
Merge branch 'main' into feat/cli-json-flag
jasonkuhrt Dec 13, 2024
b5f78b4
work
jasonkuhrt Dec 13, 2024
31eb025
Merge branch 'main' into feat/cli-json-flag
jasonkuhrt Dec 13, 2024
27e8811
not this PR
jasonkuhrt Dec 13, 2024
195e466
not this pr
jasonkuhrt Dec 13, 2024
3328e20
not this pr
jasonkuhrt Dec 13, 2024
f81c69c
undo
jasonkuhrt Dec 13, 2024
2423a50
unused
jasonkuhrt Dec 13, 2024
d4bd1bf
progress, introduce envelope
jasonkuhrt Dec 13, 2024
35da5f6
more commands
jasonkuhrt Dec 13, 2024
fbc286e
refactor
jasonkuhrt Dec 13, 2024
6a4a46b
clarify log methods
jasonkuhrt Dec 13, 2024
1e3c14f
validate
jasonkuhrt Dec 13, 2024
8f68b79
Merge branch 'main' into feat/cli-json-flag
jasonkuhrt Dec 13, 2024
c9628bb
feedback: remove unused command
jasonkuhrt Dec 16, 2024
528bad9
feedback: remove unused command reset
jasonkuhrt Dec 16, 2024
e8b65ec
feedback: remove unused commands get, delete
jasonkuhrt Dec 16, 2024
fe15732
Merge branch 'main' into feat/cli-json-flag
jasonkuhrt Dec 16, 2024
0d5d33f
feedback: switch to typebox
jasonkuhrt Dec 16, 2024
333095c
doc
jasonkuhrt Dec 16, 2024
79f7763
refactor
jasonkuhrt Dec 16, 2024
1fbbcc7
todo
jasonkuhrt Dec 16, 2024
cf6074c
todo
jasonkuhrt Dec 16, 2024
f8986a6
work
jasonkuhrt Dec 16, 2024
754526c
fix
jasonkuhrt Dec 16, 2024
321325d
lint
jasonkuhrt Dec 16, 2024
923c422
remove command-wide try-catch
jasonkuhrt Dec 16, 2024
032ff42
fixes
jasonkuhrt Dec 16, 2024
ba14c2d
refactor
jasonkuhrt Dec 16, 2024
ad7e65c
finish check
jasonkuhrt Dec 16, 2024
d4f1b88
fix err msg
jasonkuhrt Dec 16, 2024
392ad0f
lint
jasonkuhrt Dec 16, 2024
6b9aba5
fix
jasonkuhrt Dec 16, 2024
af8721b
start tackling failures
jasonkuhrt Dec 16, 2024
def2053
single output
jasonkuhrt Dec 17, 2024
d631f8b
refactor
jasonkuhrt Dec 17, 2024
219faf5
refactor
jasonkuhrt Dec 17, 2024
58b62b8
progress
jasonkuhrt Dec 17, 2024
dee2049
build updates
jasonkuhrt Dec 17, 2024
2221fa0
fix import paths
jasonkuhrt Dec 17, 2024
6d498eb
lint
jasonkuhrt Dec 17, 2024
b258db2
no ci check for now
jasonkuhrt Dec 17, 2024
97c46dd
prettier
jasonkuhrt Dec 17, 2024
9439aa7
Merge branch 'main' into feat/cli-json-flag
jasonkuhrt Dec 17, 2024
612aaa4
schema to own dir
jasonkuhrt Dec 17, 2024
7c1823e
refactor
jasonkuhrt Dec 17, 2024
3ccd617
look at fragments
jasonkuhrt Dec 17, 2024
a45f5d6
output helper
jasonkuhrt Dec 17, 2024
84833da
try schema match hive
jasonkuhrt Dec 17, 2024
2b8c7a8
more
jasonkuhrt Dec 17, 2024
888657a
work
jasonkuhrt Dec 17, 2024
87fe7c7
file
jasonkuhrt Dec 17, 2024
4d5644f
publish command
jasonkuhrt Dec 17, 2024
7dffe2e
helper methods for data only
jasonkuhrt Dec 18, 2024
b33e865
runResult all
jasonkuhrt Dec 18, 2024
5796094
refacotr
jasonkuhrt Dec 18, 2024
247cf5c
prettier
jasonkuhrt Dec 18, 2024
35efce1
improve jsdoc
jasonkuhrt Dec 18, 2024
b0e966e
rename
jasonkuhrt Dec 18, 2024
45af397
Merge branch 'main' into feat/cli-json-flag
jasonkuhrt Dec 18, 2024
e15c9cf
work
jasonkuhrt Dec 18, 2024
4861b9e
fix
jasonkuhrt Dec 18, 2024
eb0b1e1
no only
jasonkuhrt Dec 18, 2024
609d286
refactor
jasonkuhrt Dec 18, 2024
1fa5079
no only
jasonkuhrt Dec 18, 2024
4b3675f
titles
jasonkuhrt Dec 18, 2024
3010a25
refactor
jasonkuhrt Dec 18, 2024
58d3c70
lint
jasonkuhrt Dec 18, 2024
9d0fc87
refactor cli tests to use inferred hive cli library
jasonkuhrt Dec 18, 2024
31cdc7c
lint
jasonkuhrt Dec 18, 2024
76eb192
no empty
jasonkuhrt Dec 18, 2024
5489204
support multi flags
jasonkuhrt Dec 19, 2024
8b23d0e
do not repeat dev interface
jasonkuhrt Dec 19, 2024
77db99a
rename
jasonkuhrt Dec 19, 2024
59b066c
rest
jasonkuhrt Dec 19, 2024
2e675fd
undo test refactors
jasonkuhrt Dec 19, 2024
438941c
Merge branch 'main' into feat/cli-json-flag
jasonkuhrt Dec 19, 2024
ee1dcc7
vitest fixtures return
jasonkuhrt Dec 19, 2024
044835a
type fix
jasonkuhrt Dec 19, 2024
ba94eea
tmpfile util
jasonkuhrt Dec 19, 2024
cd30c27
wire up json
jasonkuhrt Dec 19, 2024
90afcf7
snapshot system
jasonkuhrt Dec 19, 2024
d9b114d
all tests snapping
jasonkuhrt Dec 19, 2024
728af1f
clean
jasonkuhrt Dec 19, 2024
8cf2893
no ansi
jasonkuhrt Dec 19, 2024
764fb20
first pass of json output
jasonkuhrt Dec 19, 2024
64d60d8
json error output
jasonkuhrt Dec 19, 2024
028cfde
more json error output
jasonkuhrt Dec 19, 2024
4fb7f07
simplify
jasonkuhrt Dec 19, 2024
07fa446
optional
jasonkuhrt Dec 19, 2024
5d62044
Merge branch 'main' into feat/cli-json-flag
jasonkuhrt Dec 19, 2024
a503c12
no stacks
jasonkuhrt Dec 19, 2024
35e299e
type failure or success
jasonkuhrt Dec 19, 2024
fd1a85c
no __typename
jasonkuhrt Dec 19, 2024
c35833d
Merge branch 'main' into feat/cli-json-flag
jasonkuhrt Dec 19, 2024
07b9f97
encode type field
jasonkuhrt Dec 19, 2024
5049eb3
simplify test
jasonkuhrt Dec 19, 2024
b774dcf
try fix
jasonkuhrt Dec 19, 2024
b3b63d2
try fix
jasonkuhrt Dec 20, 2024
f7e3ef0
reduce methods
jasonkuhrt Dec 20, 2024
5444780
unused
jasonkuhrt Dec 20, 2024
af41791
try again
jasonkuhrt Dec 20, 2024
48973fb
try again
jasonkuhrt Dec 20, 2024
f1f6b9f
try one more thing
jasonkuhrt Dec 20, 2024
7943697
no parameters stuff
jasonkuhrt Dec 20, 2024
f6b12a4
format
jasonkuhrt Dec 20, 2024
c7c629e
try quick cli tests in ci
jasonkuhrt Dec 20, 2024
1cd5734
try ci run again
jasonkuhrt Dec 20, 2024
ada91d0
log settings
jasonkuhrt Dec 20, 2024
ade8ad3
pnpm pach fml
jasonkuhrt Dec 20, 2024
ad3024b
repatch
jasonkuhrt Dec 20, 2024
1f2f2eb
repatch
jasonkuhrt Dec 20, 2024
c8dbfaf
repatch
jasonkuhrt Dec 20, 2024
ed962e7
repatch
jasonkuhrt Dec 20, 2024
3d6b2e7
repatch
jasonkuhrt Dec 20, 2024
ae88819
repatch
jasonkuhrt Dec 20, 2024
66d174c
repatch
jasonkuhrt Dec 20, 2024
d2bfa72
more debug
jasonkuhrt Dec 20, 2024
dae187d
repatch
jasonkuhrt Dec 20, 2024
6663144
repatch
jasonkuhrt Dec 20, 2024
45362ae
found it?
jasonkuhrt Dec 20, 2024
71d1bd0
restore
jasonkuhrt Dec 20, 2024
bda183b
restore
jasonkuhrt Dec 20, 2024
33e5025
lint
jasonkuhrt Dec 20, 2024
aaf34ac
update vitest for no flicker watch mode
jasonkuhrt Dec 20, 2024
2457c33
restore old patch
jasonkuhrt Dec 20, 2024
c5be266
rename types
jasonkuhrt Dec 21, 2024
0b7aff4
lockfile
jasonkuhrt Dec 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,8 @@
["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"],
["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"],
["clsx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
]
],
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
jasonkuhrt marked this conversation as resolved.
Show resolved Hide resolved
}
108 changes: 93 additions & 15 deletions packages/libraries/cli/src/base-command.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
import colors from 'colors';
import { print, type GraphQLError } from 'graphql';
import type { ExecutionResult } from 'graphql';
import { z } from 'zod';
import { http } from '@graphql-hive/core';
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
import { Command, Errors, Flags, Interfaces } from '@oclif/core';
import { Config, GetConfigurationValueType, ValidConfigurationKeys } from './helpers/config';
import { OmitNever, OptionalizePropertyUnsafe } from './helpers/general';

Check failure on line 9 in packages/libraries/cli/src/base-command.ts

View workflow job for this annotation

GitHub Actions / code-style / eslint-and-prettier

'OptionalizePropertyUnsafe' is defined but never used. Allowed unused vars must match /^_/u
import { OutputSchema } from './helpers/output-schema';

export type Flags<T extends typeof Command> = Interfaces.InferredFlags<
(typeof BaseCommand)['baseFlags'] & T['flags']
>;
export type Args<T extends typeof Command> = Interfaces.InferredArgs<T['args']>;
export default abstract class BaseCommand<$Command extends typeof Command> extends Command {
jasonkuhrt marked this conversation as resolved.
Show resolved Hide resolved
public static enableJsonFlag = true;

type OmitNever<T> = { [K in keyof T as T[K] extends never ? never : K]: T[K] };
/**
* The data type returned by this command when successfully executed.
*
* Used by the {@link BaseCommand.successData} method.
*/
public static SuccessSchema: OutputSchema = OutputSchema.Envelope;

/**
* Whether to validate the data returned by the {@link BaseCommand.successData} method.
*
* WARNING: If disabling validation, then you must not any of Zod's value coercing features
* since they won't be run when validation is disabled.
*
* @defaultValue `true`
*/
public SuccessSchemaValidationEnabled: boolean = true;

export default abstract class BaseCommand<T extends typeof Command> extends Command {
protected _userConfig: Config | undefined;

static baseFlags = {
Expand All @@ -23,8 +38,9 @@
}),
};

protected flags!: Flags<T>;
protected args!: Args<T>;
protected flags!: Flags<$Command>;

protected args!: Args<$Command>;

protected get userConfig(): Config {
if (!this._userConfig) {
Expand All @@ -33,7 +49,7 @@
return this._userConfig!;
}

public async init(): Promise<void> {
async init(): Promise<void> {
await super.init();

this._userConfig = new Config({
Expand All @@ -45,26 +61,66 @@
const { args, flags } = await this.parse({
flags: this.ctor.flags,
baseFlags: (super.ctor as typeof BaseCommand).baseFlags,
enableJsonFlag: this.ctor.enableJsonFlag,
args: this.ctor.args,
strict: this.ctor.strict,
});
this.flags = flags as Flags<T>;
this.args = args as Args<T>;
this.flags = flags as Flags<$Command>;
this.args = args as Args<$Command>;
}

success(...args: any[]) {
/**
* Helper function for creating data that adheres to the type specified by your command's {@link BaseCommand.SuccessSchema}.
*
* If {@link BaseCommand.SuccessSchemaValidationEnabled} is `true`, then the given data will be runtime-validated too.
*
* For ease of use some standard properties are added for you automatically, simplifying the input you have to provide.
*/
successData(dataInput: InferSuccessDataInput<$Command>): InferSuccessDataOutput<$Command> {
const dataOutput = {
...(dataInput as object),
ok: true,
// warnings: [] as string[],
} as InferSuccessDataOutput<$Command>;

if (this.SuccessSchemaValidationEnabled) {
// @ts-expect-error TS doesn't support static property access on this.constructor for some reason.
const schema = this.constructor.SuccessSchema as OutputSchema;
const result = schema.safeParse(dataOutput);
if (!result.success) {
throw new Errors.CLIError(result.error.message);
}
return result.data as InferSuccessDataOutput<$Command>;
}

return dataOutput;
}

/**
* {@link Command.log} with success styling.
*/
logSuccess(...args: any[]) {
this.log(colors.green('✔'), ...args);
}

fail(...args: any[]) {
/**
* {@link Command.log} with failure styling.
*/
logFail(...args: any[]) {
this.log(colors.red('✖'), ...args);
}

info(...args: any[]) {
/**
* {@link Command.log} with info styling.
*/
logInfo(...args: any[]) {
this.log(colors.yellow('ℹ'), ...args);
}

infoWarning(...args: any[]) {
/**
* {@link Command.log} with warning styling.
*/
logWarning(...args: any[]) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed these to make our commands more readable. For example this.fail versus this.error have very different behaviour but that's not clear, nor is it easy to remember which is which.

this.log(colors.yellow('⚠'), ...args);
}

Expand Down Expand Up @@ -304,6 +360,28 @@
}
}

export type Flags<T extends typeof Command> = Interfaces.InferredFlags<
(typeof BaseCommand)['baseFlags'] & T['flags']
>;

export type Args<T extends typeof Command> = Interfaces.InferredArgs<T['args']>;

type InferSuccessDataOutput<$CommandClass extends typeof Command> =
'SuccessSchema' extends keyof $CommandClass
? $CommandClass['SuccessSchema'] extends OutputSchema
? z.infer<$CommandClass['SuccessSchema']>
: never
: never;

type InferSuccessDataInput<$CommandClass extends typeof Command> =
'SuccessSchema' extends keyof $CommandClass
? $CommandClass['SuccessSchema'] extends OutputSchema
? Omit<z.infer<$CommandClass['SuccessSchema']>, 'ok'>
: InferSuccessDataInputError
: InferSuccessDataInputError;

type InferSuccessDataInputError = 'Error: Missing `static SuccessSchema = ...` on your command.';

function isClientError(error: Error): error is ClientError {
return error instanceof ClientError;
}
46 changes: 38 additions & 8 deletions packages/libraries/cli/src/commands/app/create.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { OutputSchema } from 'src/helpers/output-schema';
import { z } from 'zod';
import { Args, Flags } from '@oclif/core';
import Command from '../../base-command';
Expand All @@ -6,6 +7,22 @@ import { AppDeploymentStatus } from '../../gql/graphql';
import { graphqlEndpoint } from '../../helpers/config';

export default class AppCreate extends Command<typeof AppCreate> {
static SuccessSchema = z.union([
OutputSchema.Effect.Skipped.extend({
data: z.object({
// TODO improve type to match GQL Schema enum
status: OutputSchema.NonEmptyString,
}),
}),
OutputSchema.Effect.Executed.extend({
data: z.object({
id: OutputSchema.NonEmptyString,
name: OutputSchema.NonEmptyString,
version: OutputSchema.NonEmptyString,
}),
}),
]);

static description = 'create an app deployment';
static flags = {
'registry.endpoint': Flags.string({
Expand Down Expand Up @@ -79,10 +96,15 @@ export default class AppCreate extends Command<typeof AppCreate> {
}

if (result.createAppDeployment.ok.createdAppDeployment.status !== AppDeploymentStatus.Pending) {
this.log(
`App deployment "${flags['name']}@${flags['version']}" is "${result.createAppDeployment.ok.createdAppDeployment.status}". Skip uploading documents...`,
);
return;
const message = `App deployment "${flags['name']}@${flags['version']}" is "${result.createAppDeployment.ok.createdAppDeployment.status}". Skip uploading documents...`;
this.log(message);
return this.successData({
message,
effect: 'skipped',
data: {
status: result.createAppDeployment.ok.createdAppDeployment.status,
},
});
}

let buffer: Array<{ hash: string; body: string }> = [];
Expand Down Expand Up @@ -114,7 +136,7 @@ export default class AppCreate extends Command<typeof AppCreate> {
? affectedOperation.body.substring(0, maxCharacters) + '...'
: affectedOperation.body
).replace(/\n/g, '\\n');
this.infoWarning(
this.logWarning(
`Failed uploading document: ${result.addDocumentsToAppDeployment.error.details.message}` +
`\nOperation hash: ${affectedOperation?.hash}` +
`\nOperation body: ${truncatedBody}`,
Expand All @@ -137,9 +159,17 @@ export default class AppCreate extends Command<typeof AppCreate> {

await flush(true);

this.log(
`\nApp deployment "${flags['name']}@${flags['version']}" (${counter} operations) created.\nActive it with the "hive app:publish" command.`,
);
const message = `App deployment "${flags['name']}@${flags['version']}" (${counter} operations) created.\nActivate it with the "hive app:publish" command.`;
this.log(message);
return this.successData({
message,
effect: 'executed',
data: {
id: result.createAppDeployment.ok.createdAppDeployment.id,
name: flags['name'],
version: flags['version'],
},
});
}
}

Expand Down
36 changes: 33 additions & 3 deletions packages/libraries/cli/src/commands/app/publish.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
import { OutputSchema } from 'src/helpers/output-schema';
import { z } from 'zod';
import { Flags } from '@oclif/core';
import Command from '../../base-command';
import { graphql } from '../../gql';
import { graphqlEndpoint } from '../../helpers/config';

export default class AppPublish extends Command<typeof AppPublish> {
static SuccessSchema = z.union([
OutputSchema.Effect.Skipped.extend({
data: z.object({
name: OutputSchema.NonEmptyString,
}),
}),
OutputSchema.Effect.Executed.extend({
data: z.object({
name: OutputSchema.NonEmptyString,
}),
}),
]);

static description = 'publish an app deployment';
static flags = {
'registry.endpoint': Flags.string({
Expand Down Expand Up @@ -55,10 +70,25 @@ export default class AppPublish extends Command<typeof AppPublish> {
const name = `${result.activateAppDeployment.ok.activatedAppDeployment.name}@${result.activateAppDeployment.ok.activatedAppDeployment.version}`;

if (result.activateAppDeployment.ok.isSkipped) {
this.warn(`App deployment "${name}" is already published. Skipping...`);
return;
const message = `App deployment "${name}" is already published. Skipping...`;
this.warn(message);
return this.successData({
message,
effect: 'skipped',
data: {
name,
},
});
}
this.log(`App deployment "${name}" published successfully.`);
const message = `App deployment "${name}" published successfully.`;
this.log(message);
return this.successData({
message,
effect: 'executed',
data: {
name,
},
});
}
}
}
Expand Down
31 changes: 28 additions & 3 deletions packages/libraries/cli/src/commands/artifact/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import { OutputSchema } from 'src/helpers/output-schema';
import { z } from 'zod';
import { http, URL } from '@graphql-hive/core';
import { Flags } from '@oclif/core';
import Command from '../../base-command';

export default class ArtifactsFetch extends Command<typeof ArtifactsFetch> {
static SuccessSchema = z.union([
OutputSchema.Envelope.extend({
data: OutputSchema.DataOutputMode.File,
}),
OutputSchema.Envelope.extend({
data: OutputSchema.DataOutputMode.Stdout,
}),
]);

static description = 'fetch artifacts from the CDN';
static flags = {
'cdn.endpoint': Flags.string({
Expand Down Expand Up @@ -69,10 +80,24 @@ export default class ArtifactsFetch extends Command<typeof ArtifactsFetch> {
const fs = await import('fs/promises');
const contents = Buffer.from(await response.arrayBuffer());
await fs.writeFile(flags.outputFile, contents);
this.log(`Wrote ${contents.length} bytes to ${flags.outputFile}`);
return;
const message = `Wrote ${contents.length} bytes to ${flags.outputFile}`;
this.log(message);
return this.successData({
message,
data: {
outputMode: 'file',
path: flags.outputFile,
},
});
}

this.log(await response.text());
const content = await response.text();
this.log(content);
return this.successData({
data: {
outputMode: 'stdout',
content,
},
});
}
}
20 changes: 18 additions & 2 deletions packages/libraries/cli/src/commands/config/delete.ts
jasonkuhrt marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { OutputSchema } from 'src/helpers/output-schema';
import { z } from 'zod';
import { Args } from '@oclif/core';
import Command from '../../base-command';

export default class DeleteConfig extends Command<typeof DeleteConfig> {
static description = 'deletes specific cli configuration';
static SuccessSchema = OutputSchema.Envelope.extend({
data: z.object({
key: OutputSchema.NonEmptyString,
}),
});
static description = 'delete a specific cli configuration key';
static args = {
// TODO Terms like "property" or "setting" are more clear.
// "key" is overloaded with authN/authZ concepts.
key: Args.string({
name: 'key',
required: true,
Expand All @@ -14,6 +23,13 @@ export default class DeleteConfig extends Command<typeof DeleteConfig> {
async run() {
const { args } = await this.parse(DeleteConfig);
this._userConfig!.delete(args.key);
this.success(this.bolderize(`Config flag "${args.key}" was deleted`));
const message = `Config key "${args.key}" was deleted`;
this.logSuccess(this.bolderize(message));
return this.successData({
message,
data: {
key: args.key,
},
});
}
}
Loading
Loading