Skip to content

Commit

Permalink
renderable data types
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonkuhrt committed Dec 21, 2024
1 parent dacb9a9 commit 83f9083
Show file tree
Hide file tree
Showing 19 changed files with 300 additions and 210 deletions.
12 changes: 6 additions & 6 deletions integration-tests/tests/cli/__snapshots__/schema.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ stdout--------------------------------------------:
"reference": null,
"suggestions": [],
"data": {
"type": "HiveApiRequestError",
"type": "FailureHiveApiRequest",
"message": "Request to Hive API failed. Caused by error(s):\\nInvalid token provided",
"requestId": "__ID__",
"errors": [
Expand Down Expand Up @@ -718,7 +718,7 @@ stdout--------------------------------------------:
"reference": null,
"suggestions": [],
"data": {
"type": "HiveApiRequestError",
"type": "FailureHiveApiRequest",
"message": "Request to Hive API failed. Caused by error(s):\\nInvalid token provided",
"requestId": "__ID__",
"errors": [
Expand Down Expand Up @@ -1036,7 +1036,7 @@ stdout--------------------------------------------:
"reference": null,
"suggestions": [],
"data": {
"type": "HiveApiRequestError",
"type": "FailureHiveApiRequest",
"message": "Request to Hive API failed. Caused by error(s):\\nInvalid token provided",
"requestId": "__ID__",
"errors": [
Expand Down Expand Up @@ -1354,7 +1354,7 @@ stdout--------------------------------------------:
"reference": null,
"suggestions": [],
"data": {
"type": "HiveApiRequestError",
"type": "FailureHiveApiRequest",
"message": "Request to Hive API failed. Caused by error(s):\\nInvalid token provided",
"requestId": "__ID__",
"errors": [
Expand Down Expand Up @@ -1723,7 +1723,7 @@ stdout--------------------------------------------:
"reference": null,
"suggestions": [],
"data": {
"type": "HiveApiRequestError",
"type": "FailureHiveApiRequest",
"message": "Request to Hive API failed. Caused by error(s):\\nInvalid token provided",
"requestId": "__ID__",
"errors": [
Expand Down Expand Up @@ -2086,7 +2086,7 @@ stdout--------------------------------------------:
"reference": null,
"suggestions": [],
"data": {
"type": "HiveApiRequestError",
"type": "FailureHiveApiRequest",
"message": "Request to Hive API failed. Caused by error(s):\\nInvalid token provided",
"requestId": "__ID__",
"errors": [
Expand Down
68 changes: 46 additions & 22 deletions packages/libraries/cli/src/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@ import type { ExecutionResult } from 'graphql';
import { http } from '@graphql-hive/core';
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
import { Command, Flags, Interfaces } from '@oclif/core';
import { ParserOutput } from '@oclif/core/lib/interfaces/parser';
import { Config, GetConfigurationValueType, ValidConfigurationKeys } from './helpers/config';
import { Errors } from './helpers/errors/__';
import { CLIErrorWithData } from './helpers/errors/cli-error-with-data';
import { OmitNever } from './helpers/general';
import { Tex } from './helpers/tex/__';
import { tb } from './helpers/typebox/__';
import { SchemaOutput } from './schema-output/__';

export type InferInput<T extends typeof Command> = Pick<
ParserOutput<T['flags'], T['baseFlags'], T['args']>,
'args' | 'flags'
>;

export default abstract class BaseCommand<$Command extends typeof Command> extends Command {
public static enableJsonFlag = true;

Expand All @@ -18,7 +25,7 @@ export default abstract class BaseCommand<$Command extends typeof Command> exten
*
* Used by methods: {@link BaseCommand.success}, {@link BaseCommand.failure}, {@link BaseCommand.runResult}.
*/
public static output: SchemaOutput.OutputBaseT = SchemaOutput.OutputBase;
public static output: SchemaOutput.OutputDataType[] = [];

protected _userConfig: Config | undefined;

Expand All @@ -39,37 +46,58 @@ export default abstract class BaseCommand<$Command extends typeof Command> exten
* By default this command runs {@link BaseCommand.runResult}, having logic to handle its return value.
*/
async run(): Promise<void | SchemaOutput.InferSuccess<GetOutput<$Command>>> {
// todo: Make it easier for the Hive team to be alerted.
// - Alert the Hive team automatically with some opt-in telemetry?
// - A single-click-link with all relevant variables serialized into search parameters?
const schemaViolationMessage = `Whoops. This Hive CLI command tried to output a value that violates its own schema. This should never happen. Please report this error to the Hive team at https://github.com/graphql-hive/console/issues/new.`;

const thisClass = this.constructor as typeof BaseCommand;
const resultUnparsed = await this.runResult();
const schema = (this.constructor as typeof BaseCommand).output as SchemaOutput.OutputBaseT;
// @ts-expect-error fixme
const resultDataTypeName = resultUnparsed.data.type;
const dataType = thisClass.output.find(
dataType => dataType.schema.properties.data.properties.type.const === resultDataTypeName,
);
if (!dataType) {
throw new CLIErrorWithData({
message: schemaViolationMessage,
data: {
type: 'ErrorDataTypeNotFound',
message: schemaViolationMessage,
value: resultUnparsed,
},
});
}

const errorsIterator = tb.Value.Value.Errors(schema, resultUnparsed);
const errorsIterator = tb.Value.Value.Errors(dataType.schema, resultUnparsed);
const materializedErrors = tb.Value.MaterializeValueErrorIterator(errorsIterator);
if (materializedErrors.length > 0) {
// todo: Make it easier for the Hive team to be alerted.
// - Alert the Hive team automatically with some opt-in telemetry?
// - A single-click-link with all relevant variables serialized into search parameters?
const message = `Whoops. This Hive CLI command tried to output a value that violates its own schema. This should never happen. Please report this error to the Hive team at https://github.com/graphql-hive/console/issues/new.`;
// todo: Display data in non-json output.
// The default textual output of an OClif error will not display any of the data below. We will want that information in a bug report.
throw new Errors.CLIErrorWithData({
message,
message: schemaViolationMessage,
data: {
type: 'ErrorOutputSchemaViolation',
message,
schema: schema,
message: schemaViolationMessage,
schema: dataType,
value: resultUnparsed,
errors: materializedErrors,
},
});
}

// Should never throw because we checked for errors above.
const result = tb.Value.Parse(schema, resultUnparsed);
const result = tb.Value.Parse(dataType.schema, resultUnparsed);

// Data types can optionally bundle a textual representation of their data.
if (dataType.render) {
this.log(dataType.render({ flags: this.flags, args: this.args }, result.data));
}

/**
* OClif outputs returned values as JSON.
*/
if (SchemaOutput.isSuccess(result)) {
if (SchemaOutput.isSuccess(result as any)) {
return result as any;
}

Expand Down Expand Up @@ -417,43 +445,39 @@ export default abstract class BaseCommand<$Command extends typeof Command> exten
if (value instanceof Errors.FailedFlagValidationError) {
return this.failureEnvelope({
suggestions: value.suggestions,
// @ts-expect-error fixme
data: {
type: 'FailureUserInput',
message: value.message,
problem: 'namedArgumentInvalid',
},
});
} as any);
}

if (value instanceof Errors.RequiredArgsError) {
return this.failureEnvelope({
suggestions: value.suggestions,
// @ts-expect-error fixme
data: {
type: 'FailureUserInput',
message: value.message,
problem: 'positionalArgumentMissing',
},
});
} as any);
}

if (value instanceof Errors.CLIError) {
return this.failureEnvelope({
suggestions: value.suggestions,
// @ts-expect-error fixme
data: {
type: 'Failure',
message: value.message,
},
});
} as any);
}
if (value instanceof Error) {
return this.failure({
// @ts-expect-error fixme
type: 'Failure',
message: value.message,
});
} as any);
}
return super.toErrorJson(value);
}
Expand Down Expand Up @@ -550,8 +574,8 @@ type InferOutputSuccessData<$CommandClass extends typeof Command> =
// prettier-ignore
type GetOutput<$CommandClass extends typeof Command> =
'output' extends keyof $CommandClass
? $CommandClass['output'] extends SchemaOutput.OutputBaseT
? $CommandClass['output']
? $CommandClass['output'] extends SchemaOutput.OutputDataType[]
? $CommandClass['output'][number]
: never
: never;

Expand Down
31 changes: 21 additions & 10 deletions packages/libraries/cli/src/commands/app/create.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Args, Flags } from '@oclif/core';
import Command from '../../base-command';
import Command, { InferInput } from '../../base-command';
import { graphql } from '../../gql';
import { graphqlEndpoint } from '../../helpers/config';
import { SchemaHive } from '../../helpers/schema';
Expand Down Expand Up @@ -32,20 +32,31 @@ export default class AppCreate extends Command<typeof AppCreate> {
hidden: false,
}),
};
static output = SchemaOutput.output(
static output = [
SchemaOutput.success('SuccessSkipAppCreate', {
status: SchemaOutput.AppDeploymentStatus,
schema: {
status: SchemaOutput.AppDeploymentStatus,
},
render(input: InferInput<typeof AppCreate>, output) {
return `App deployment "${input.flags.name}@${input.flags.version}" is "${output.status}". Skip uploading documents...`;
},
}),
SchemaOutput.success('SuccessAppCreate', {
id: tb.StringNonEmpty,
schema: {
id: tb.StringNonEmpty,
},
}),
SchemaOutput.failure('FailureAppCreate', {
message: tb.String(),
schema: {
message: tb.String(),
},
}),
SchemaOutput.failure('FailureInvalidManifestModel', {
errors: tb.Array(tb.Value.MaterializedValueErrorT),
schema: {
errors: tb.Array(tb.Value.MaterializedValueErrorT),
},
}),
);
];

async runResult() {
const { flags, args } = await this.parse(AppCreate);
Expand Down Expand Up @@ -98,9 +109,9 @@ export default class AppCreate extends Command<typeof AppCreate> {
}

if (result.ok.createdAppDeployment.status !== SchemaHive.AppDeploymentStatus.Pending) {
this.log(
`App deployment "${flags['name']}@${flags['version']}" is "${result.ok.createdAppDeployment.status}". Skip uploading documents...`,
);
// this.log(
// `App deployment "${flags['name']}@${flags['version']}" is "${result.ok.createdAppDeployment.status}". Skip uploading documents...`,
// );
return this.success({
type: 'SuccessSkipAppCreate',
status: result.ok.createdAppDeployment.status,
Expand Down
20 changes: 13 additions & 7 deletions packages/libraries/cli/src/commands/app/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,25 @@ export default class AppPublish extends Command<typeof AppPublish> {
required: true,
}),
};
static output = SchemaOutput.output(
static output = [
SchemaOutput.success('SuccessSkipAppPublish', {
name: tb.StringNonEmpty,
version: tb.StringNonEmpty,
schema: {
name: tb.StringNonEmpty,
version: tb.StringNonEmpty,
},
}),
SchemaOutput.success('SuccessAppPublish', {
name: tb.StringNonEmpty,
version: tb.StringNonEmpty,
schema: {
name: tb.StringNonEmpty,
version: tb.StringNonEmpty,
},
}),
SchemaOutput.failure('FailureAppPublish', {
message: tb.String(),
schema: {
message: tb.String(),
},
}),
);
];

async runResult() {
const { flags } = await this.parse(AppPublish);
Expand Down
5 changes: 1 addition & 4 deletions packages/libraries/cli/src/commands/artifact/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@ export default class ArtifactsFetch extends Command<typeof ArtifactsFetch> {
description: 'whether to write to a file instead of stdout',
}),
};
static output = SchemaOutput.output(
SchemaOutput.SuccessOutputFile,
SchemaOutput.SuccessOutputStdout,
);
static output = [SchemaOutput.SuccessOutputFile, SchemaOutput.SuccessOutputStdout];

async runResult() {
const { flags } = await this.parse(ArtifactsFetch);
Expand Down
5 changes: 1 addition & 4 deletions packages/libraries/cli/src/commands/introspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@ export default class Introspect extends Command<typeof Introspect> {
hidden: false,
}),
};
static output = SchemaOutput.output(
SchemaOutput.SuccessOutputFile,
SchemaOutput.SuccessOutputStdout,
);
static output = [SchemaOutput.SuccessOutputFile, SchemaOutput.SuccessOutputStdout];

async runResult() {
const { flags, args } = await this.parse(Introspect);
Expand Down
50 changes: 26 additions & 24 deletions packages/libraries/cli/src/commands/operations/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,33 +78,35 @@ export default class OperationsCheck extends Command<typeof OperationsCheck> {
hidden: false,
}),
};
static output = SchemaOutput.output(
SchemaOutput.failure('FailureOperationsCheckNoSchemaFound', {}),
SchemaOutput.success('SuccessOperationsCheckNoOperationsFound', {}),
static output = [
SchemaOutput.failure('FailureOperationsCheckNoSchemaFound', { schema: {} }),
SchemaOutput.success('SuccessOperationsCheckNoOperationsFound', { schema: {} }),
SchemaOutput.success('SuccessOperationsCheck', {
countTotal: tb.Integer({ minimum: 0 }),
countInvalid: tb.Integer({ minimum: 0 }),
countValid: tb.Integer({ minimum: 0 }),
invalidOperations: tb.Array(
tb.Object({
source: tb.Object({
name: tb.String(),
}),
errors: tb.Array(
tb.Object({
message: tb.String(),
locations: tb.Array(
tb.Object({
line: tb.Integer({ minimum: 0 }),
column: tb.Integer({ minimum: 0 }),
}),
),
schema: {
countTotal: tb.Integer({ minimum: 0 }),
countInvalid: tb.Integer({ minimum: 0 }),
countValid: tb.Integer({ minimum: 0 }),
invalidOperations: tb.Array(
tb.Object({
source: tb.Object({
name: tb.String(),
}),
),
}),
),
errors: tb.Array(
tb.Object({
message: tb.String(),
locations: tb.Array(
tb.Object({
line: tb.Integer({ minimum: 0 }),
column: tb.Integer({ minimum: 0 }),
}),
),
}),
),
}),
),
},
}),
);
];

async runResult() {
const { flags, args } = await this.parse(OperationsCheck);
Expand Down
Loading

0 comments on commit 83f9083

Please sign in to comment.