Skip to content

Commit

Permalink
feat(internal): Implement initial multi-schema utilities (0no-co#257)
Browse files Browse the repository at this point in the history
  • Loading branch information
kitten authored Apr 25, 2024
1 parent 83eb15d commit 3c09253
Show file tree
Hide file tree
Showing 22 changed files with 311 additions and 45 deletions.
5 changes: 5 additions & 0 deletions .changeset/nice-pears-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@gql.tada/internal": minor
---

Add multi-schema config format and schema loading.
5 changes: 5 additions & 0 deletions packages/cli-utils/src/commands/check/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ export async function* run(tty: TTY, opts: Options): AsyncIterable<ComposeInput>
throw logger.externalError('Failed to load configuration.', error);
}

if (!('schema' in pluginConfig)) {
// TODO: Implement multi-schema support
throw logger.errorMessage('Multi-schema support is not implemented yet');
}

const summary: SeveritySummary = { warn: 0, error: 0, info: 0 };
const minSeverity = opts.minSeverity;
const generator = runDiagnostics({
Expand Down
5 changes: 5 additions & 0 deletions packages/cli-utils/src/commands/check/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export interface DiagnosticsParams {
async function* _runDiagnostics(
params: DiagnosticsParams
): AsyncIterableIterator<DiagnosticSignal> {
if (!('schema' in params.pluginConfig)) {
// TODO: Implement multi-schema support
throw new Error('Multi-schema support is not implemented yet');
}

const projectPath = path.dirname(params.configPath);
const loader = load({ origin: params.pluginConfig.schema, rootPath: projectPath });
const factory = programFactory(params);
Expand Down
5 changes: 5 additions & 0 deletions packages/cli-utils/src/commands/doctor/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ export async function* run(): AsyncIterable<ComposeInput> {
);
}

if (!('schema' in pluginConfig)) {
// TODO: Implement multi-schema support
throw logger.errorMessage('Multi-schema support is not implemented yet');
}

if (!pluginConfig.schema) {
yield logger.failedTask(Messages.CHECK_TSCONFIG);
throw logger.errorMessage(
Expand Down
5 changes: 5 additions & 0 deletions packages/cli-utils/src/commands/generate-output/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ export async function* run(tty: TTY, opts: OutputOptions): AsyncIterable<Compose
throw logger.externalError('Failed to load configuration.', error);
}

if (!('schema' in pluginConfig)) {
// TODO: Implement multi-schema support
throw logger.errorMessage('Multi-schema support is not implemented yet');
}

// TODO: allow this to be overwritten using arguments (like in `generate schema`)
const loader = load({
origin: pluginConfig.schema,
Expand Down
5 changes: 5 additions & 0 deletions packages/cli-utils/src/commands/generate-persisted/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ export async function* run(tty: TTY, opts: PersistedOptions): AsyncIterable<Comp
throw logger.externalError('Failed to load configuration.', error);
}

if (!('schema' in pluginConfig)) {
// TODO: Implement multi-schema support
throw logger.errorMessage('Multi-schema support is not implemented yet');
}

let destination: WriteTarget;
if (!opts.output && tty.pipeTo) {
destination = tty.pipeTo;
Expand Down
5 changes: 5 additions & 0 deletions packages/cli-utils/src/commands/generate-schema/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ export async function* run(tty: TTY, opts: SchemaOptions): AsyncIterable<Compose
throw logger.externalError('Failed to load configuration.', error);
}

if (!('schema' in pluginConfig)) {
// TODO: Implement multi-schema support
throw logger.errorMessage('Multi-schema support is not implemented yet');
}

if (
typeof pluginConfig.schema === 'string' &&
path.extname(pluginConfig.schema) === '.graphql'
Expand Down
5 changes: 5 additions & 0 deletions packages/cli-utils/src/commands/turbo/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export async function* run(tty: TTY, opts: TurboOptions): AsyncIterable<ComposeI
throw logger.externalError('Failed to load configuration.', error);
}

if (!('schema' in pluginConfig)) {
// TODO: Implement multi-schema support
throw logger.errorMessage('Multi-schema support is not implemented yet');
}

let destination: WriteTarget;
if (!opts.output && tty.pipeTo) {
destination = tty.pipeTo;
Expand Down
138 changes: 111 additions & 27 deletions packages/internal/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,32 @@ import { TadaError } from './errors';
import { getURLConfig } from './loaders';
import type { SchemaOrigin } from './loaders';

export interface GraphQLSPConfig {
export interface BaseConfig {
template?: string;
}

export interface SchemaConfig {
name?: string;
schema: SchemaOrigin;
tadaOutputLocation?: string;
tadaTurboLocation?: string;
tadaPersistedLocation?: string;
template?: string;
}

export const parseConfig = (
input: unknown,
/** Defines the path of the "main" `tsconfig.json` file.
* @remarks
* This should be the `rootPath` output from `loadConfig`,
* which is the path of the user's `tsconfig.json` before
* resolving `extends` options.
*/
rootPath: string = process.cwd()
): GraphQLSPConfig => {
const SCHEMA_PROPS = [
'name',
'tadaOutputLocation',
'tadaTurboLocation',
'tadaPersistedLocation',
] as const;

interface MultiSchemaConfig extends SchemaConfig {
name: string;
}

export type GraphQLSPConfig = BaseConfig & (SchemaConfig | { schemas: MultiSchemaConfig[] });

const parseSchemaConfig = (input: unknown, rootPath: string): SchemaConfig => {
const resolveConfigDir = (input: string | undefined) => {
if (!input) return input;
return path.normalize(
Expand All @@ -37,13 +45,13 @@ export const parseConfig = (
};

if (input == null || typeof input !== 'object') {
throw new TadaError(`Configuration was not loaded properly (Received: ${input})`);
throw new TadaError(`Schema is not configured properly (Received: ${input})`);
}

if ('schema' in input && input.schema && typeof input.schema === 'object') {
const { schema } = input;
if (!('url' in schema)) {
throw new TadaError('Configuration contains a `schema` object, but no `url` property');
throw new TadaError('Schema contains a `schema` object, but no `url` property');
}

if ('headers' in schema && schema.headers && typeof schema.headers === 'object') {
Expand All @@ -55,41 +63,37 @@ export const parseConfig = (
}
}
} else if ('headers' in schema) {
throw new TadaError(
"Configuration contains a `schema.headers` property, but it's not an object"
);
throw new TadaError("Schema contains a `schema.headers` property, but it's not an object");
}
} else if (!('schema' in input) || typeof input.schema !== 'string') {
throw new TadaError('Configuration is missing a `schema` property');
}

if (!('schema' in input) || typeof input.schema !== 'string') {
throw new TadaError('Schema is missing a `schema` property');
} else if (
'tadaOutputLocation' in input &&
input.tadaOutputLocation &&
typeof input.tadaOutputLocation !== 'string'
) {
throw new TadaError(
"Configuration contains a `tadaOutputLocation` property, but it's not a file path"
"Schema contains a `tadaOutputLocation` property, but it's not a file path"
);
} else if (
'tadaTurboLocation' in input &&
input.tadaTurboLocation &&
typeof input.tadaTurboLocation !== 'string'
) {
throw new TadaError(
"Configuration contains a `tadaTurboLocation` property, but it's not a file path"
);
throw new TadaError("Schema contains a `tadaTurboLocation` property, but it's not a file path");
} else if (
'tadaPersistedLocation' in input &&
input.tadaPersistedLocation &&
typeof input.tadaPersistedLocation !== 'string'
) {
throw new TadaError(
"Configuration contains a `tadaPersistedLocation` property, but it's not a file path"
"Schema contains a `tadaPersistedLocation` property, but it's not a file path"
);
} else if ('template' in input && input.template && typeof input.template !== 'string') {
throw new TadaError("Configuration contains a `template` property, but it's not a string");
}

const output = input as any as GraphQLSPConfig;
const output = input as any as SchemaConfig;

let schema: SchemaOrigin = output.schema;
if (typeof schema === 'string') {
Expand All @@ -99,8 +103,88 @@ export const parseConfig = (

return {
...output,
schema,
tadaOutputLocation: resolveConfigDir(output.tadaOutputLocation),
tadaTurboLocation: resolveConfigDir(output.tadaTurboLocation),
tadaPersistedLocation: resolveConfigDir(output.tadaPersistedLocation),
};
};

export const parseConfig = (
input: unknown,
/** Defines the path of the "main" `tsconfig.json` file.
* @remarks
* This should be the `rootPath` output from `loadConfig`,
* which is the path of the user's `tsconfig.json` before
* resolving `extends` options.
*/
rootPath: string = process.cwd()
): GraphQLSPConfig => {
if (input == null || typeof input !== 'object') {
throw new TadaError(`Configuration is of an invalid type (Received: ${input})`);
} else if ('template' in input && input.template && typeof input.template !== 'string') {
throw new TadaError("Configuration contains a `template` property, but it's not a string");
} else if ('name' in input && input.name && typeof input.name !== 'string') {
throw new TadaError("Configuration contains a `name` property, but it's not a string");
}

if ('schemas' in input) {
if (!Array.isArray(input.schemas)) {
throw new TadaError("Configuration contains a `schema` property, but it's not an array");
}

if ('schema' in input) {
throw new TadaError(
'If configuration contains a `schemas` property, it cannot contain a `schema` configuration.'
);
} else if ('tadaOutputLocation' in input) {
throw new TadaError(
"If configuration contains a `schemas` property, it cannot contain a 'tadaOutputLocation` configuration."
);
} else if ('tadaTurboLocation' in input) {
throw new TadaError(
"If configuration contains a `schemas` property, it cannot contain a 'tadaTurboLocation` configuration."
);
} else if ('tadaPersistedLocation' in input) {
throw new TadaError(
"If configuration contains a `schemas` property, it cannot contain a 'tadaPersistedLocation` configuration."
);
}

const schemas = input.schemas.map((schema): MultiSchemaConfig => {
if (!('name' in schema) || !schema.name || typeof schema.name !== 'string')
throw new TadaError('All `schemas` configurations must contain a `name` label.');
return {
...parseSchemaConfig(schema, rootPath),
name: schema.name,
};
});

for (const prop of SCHEMA_PROPS) {
const values = new Set(schemas.map((schema) => schema[prop])).size;
if (values !== schemas.length)
throw new TadaError(`All '${prop}' values in 'schemas' must be unique.`);
}

return { ...input, schemas };
} else {
return { ...input, ...parseSchemaConfig(input, rootPath) };
}
};

export const getSchemaConfigForName = (
config: GraphQLSPConfig,
name: string | undefined
): SchemaConfig | null => {
if (name && 'name' in config && config.name === name) {
return config;
} else if (!name && !('schemas' in config)) {
return config;
} else if (name && 'schemas' in config) {
for (let index = 0; index < config.schemas.length; index++)
if (config.schemas[index].name === name) return config.schemas[index];
return null;
} else {
return null;
}
};
7 changes: 6 additions & 1 deletion packages/internal/src/introspection/minify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import type {
IntrospectionField,
} from 'graphql';

import type { IntrospectionResult } from '../loaders';

function nameCompare(objA: { name: string }, objB: { name: string }) {
return objA.name < objB.name ? -1 : objA.name > objB.name ? 1 : 0;
}
Expand Down Expand Up @@ -149,7 +151,9 @@ function minifyIntrospectionType(type: IntrospectionType): IntrospectionType {
* If `schema` receives an object that isn’t an {@link IntrospectionQuery}, a
* {@link TypeError} will be thrown.
*/
export const minifyIntrospectionQuery = (schema: IntrospectionQuery): IntrospectionQuery => {
export const minifyIntrospectionQuery = (
schema: IntrospectionQuery | IntrospectionResult
): IntrospectionResult => {
if (!schema || !('__schema' in schema)) {
throw new TypeError('Expected to receive an IntrospectionQuery.');
}
Expand Down Expand Up @@ -185,6 +189,7 @@ export const minifyIntrospectionQuery = (schema: IntrospectionQuery): Introspect
.sort(nameCompare);

return {
name: 'name' in schema ? schema.name : undefined,
__schema: {
queryType: {
kind: queryType.kind,
Expand Down
25 changes: 17 additions & 8 deletions packages/internal/src/introspection/output.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { IntrospectionQuery } from 'graphql';
import type { IntrospectionResult } from '../loaders';

import { TadaError } from '../errors';
import { PREAMBLE_IGNORE, ANNOTATION_DTS, ANNOTATION_TS } from './constants';
Expand All @@ -13,25 +14,33 @@ interface OutputIntrospectionFileOptions {
}

export function outputIntrospectionFile(
introspection: IntrospectionQuery | string,
introspection: IntrospectionQuery | IntrospectionResult,
opts: OutputIntrospectionFileOptions
): string {
if (/\.d\.ts$/.test(opts.fileType)) {
const json =
typeof introspection !== 'string' && opts.shouldPreprocess
? preprocessIntrospection(introspection)
: stringifyJson(introspection);
return [
const out = [
PREAMBLE_IGNORE,
ANNOTATION_DTS,
`export type introspection = ${json};\n`,
"import * as gqlTada from 'gql.tada';\n",
"declare module 'gql.tada' {",
' interface setupSchema {',
' introspection: introspection',
' }',
'}',
].join('\n');
];
// NOTE: When the `name` option is used and multiple schemas are present,
// we omit the automatic schema declaration and rely on the user calling
// `initGraphQLTada()` themselves
if (!('name' in introspection)) {
out.push(
"declare module 'gql.tada' {",
' interface setupSchema {',
' introspection: introspection',
' }',
'}'
);
}
return out.join('\n');
} else if (/\.ts$/.test(opts.fileType)) {
const json = stringifyJson(introspection);
return [
Expand Down
9 changes: 8 additions & 1 deletion packages/internal/src/introspection/preprocess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type {
IntrospectionField,
} from 'graphql';

import type { IntrospectionResult } from '../loaders';

const printName = (input: string | undefined | null): string => (input ? `'${input}'` : 'never');

const printTypeRef = (typeRef: IntrospectionTypeRef) => {
Expand Down Expand Up @@ -78,7 +80,11 @@ export const printIntrospectionType = (type: IntrospectionType) => {
}
};

export function preprocessIntrospection({ __schema: schema }: IntrospectionQuery): string {
export function preprocessIntrospection(
introspection: IntrospectionResult | IntrospectionQuery
): string {
const { __schema: schema } = introspection;
const name = 'name' in introspection ? introspection.name : undefined;
const queryName = printName(schema.queryType.name);
const mutationName = printName(schema.mutationType && schema.mutationType.name);
const subscriptionName = printName(schema.subscriptionType && schema.subscriptionType.name);
Expand All @@ -92,6 +98,7 @@ export function preprocessIntrospection({ __schema: schema }: IntrospectionQuery

return (
'{\n' +
` name: ${printName(name)};\n` +
` query: ${queryName};\n` +
` mutation: ${mutationName};\n` +
` subscription: ${subscriptionName};\n` +
Expand Down
Loading

0 comments on commit 3c09253

Please sign in to comment.