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

fix(internal): Improve loader by adding a merged loader and incorporating SchemaOrigin parsing #165

Merged
merged 9 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
6 changes: 6 additions & 0 deletions .changeset/twelve-tips-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@gql.tada/cli-utils": patch
"@gql.tada/internal": patch
---

Update internal loader to merge them into one and incorporate `SchemaOrigin` parsing
7 changes: 4 additions & 3 deletions packages/cli-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { printSchema } from 'graphql';

import type { GraphQLSchema } from 'graphql';
import type { TsConfigJson } from 'type-fest';
import { resolveTypeScriptRootDir } from '@gql.tada/internal';
import { resolveTypeScriptRootDir, load } from '@gql.tada/internal';

import { getGraphQLSPConfig } from './lsp';
import { ensureTadaIntrospection, makeLoader } from './tada';
import { ensureTadaIntrospection } from './tada';

interface GenerateSchemaOptions {
headers?: Record<string, string>;
Expand All @@ -21,7 +21,8 @@ export async function generateSchema(
target: string,
{ headers, output, cwd = process.cwd() }: GenerateSchemaOptions
) {
const loader = makeLoader(cwd, headers ? { url: target, headers } : target);
const origin = headers ? { url: target, headers } : target;
const loader = load({ origin, rootPath: cwd });

let schema: GraphQLSchema | null;
try {
Expand Down
8 changes: 1 addition & 7 deletions packages/cli-utils/src/lsp.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import type { TsConfigJson } from 'type-fest';

export type SchemaOrigin =
| string
| {
url: string;
headers: HeadersInit;
};
import type { SchemaOrigin } from '@gql.tada/internal';

export type GraphQLSPConfig = {
name: string;
Expand Down
31 changes: 4 additions & 27 deletions packages/cli-utils/src/tada.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@
import type { SchemaLoader } from '@gql.tada/internal';

import {
type SchemaOrigin,
minifyIntrospection,
outputIntrospectionFile,
loadFromSDL,
loadFromURL,
load,
} from '@gql.tada/internal';

import type { SchemaOrigin } from './lsp';

/**
* This function mimics the behavior of the LSP, this so we can ensure
* that gql.tada will work in any environment. The JetBrains IDE's do not
Expand All @@ -20,12 +18,12 @@
* this function.
*/
export async function ensureTadaIntrospection(
schemaLocation: SchemaOrigin,
origin: SchemaOrigin,
outputLocation: string,
base: string = process.cwd(),
shouldPreprocess = true
) {
const loader = makeLoader(base, schemaLocation);
const loader = load({ origin, rootPath: base });

let introspection: IntrospectionQuery | null;
try {
Expand Down Expand Up @@ -53,34 +51,13 @@
}
}

const getURLConfig = (origin: SchemaOrigin) => {
if (typeof origin === 'string') {
try {
return { url: new URL(origin) };
} catch (_error) {
return null;
}
} else if (typeof origin.url === 'string') {
try {
return {
url: new URL(origin.url),
headers: origin.headers,
};
} catch (error) {
throw new Error(`Input URL "${origin.url}" is invalid`);
}
} else {
return null;
}
};

export function makeLoader(root: string, origin: SchemaOrigin): SchemaLoader {
const urlOrigin = getURLConfig(origin);

Check failure on line 55 in packages/cli-utils/src/tada.ts

View workflow job for this annotation

GitHub Actions / Checks

Cannot find name 'getURLConfig'.

Check failure on line 55 in packages/cli-utils/src/tada.ts

View workflow job for this annotation

GitHub Actions / Checks

Cannot find name 'getURLConfig'.

Check failure on line 55 in packages/cli-utils/src/tada.ts

View workflow job for this annotation

GitHub Actions / Checks

Cannot find name 'getURLConfig'.
kitten marked this conversation as resolved.
Show resolved Hide resolved
if (urlOrigin) {
return loadFromURL(urlOrigin);

Check failure on line 57 in packages/cli-utils/src/tada.ts

View workflow job for this annotation

GitHub Actions / Checks

Cannot find name 'loadFromURL'.

Check failure on line 57 in packages/cli-utils/src/tada.ts

View workflow job for this annotation

GitHub Actions / Checks

Cannot find name 'loadFromURL'.

Check failure on line 57 in packages/cli-utils/src/tada.ts

View workflow job for this annotation

GitHub Actions / Checks

Cannot find name 'loadFromURL'.
} else if (typeof origin === 'string') {
const file = path.resolve(root, origin);
return loadFromSDL({ file, assumeValid: true });

Check failure on line 60 in packages/cli-utils/src/tada.ts

View workflow job for this annotation

GitHub Actions / Checks

Cannot find name 'loadFromSDL'.

Check failure on line 60 in packages/cli-utils/src/tada.ts

View workflow job for this annotation

GitHub Actions / Checks

Cannot find name 'loadFromSDL'.

Check failure on line 60 in packages/cli-utils/src/tada.ts

View workflow job for this annotation

GitHub Actions / Checks

Cannot find name 'loadFromSDL'.
} else {
throw new Error(`Configuration contains an invalid "schema" option`);
}
Expand Down
44 changes: 41 additions & 3 deletions packages/internal/src/loaders/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,41 @@
export type { SchemaLoader } from './types';
export { loadFromSDL } from './sdl';
export { loadFromURL } from './url';
export type * from './types';

import path from 'node:path';
import type { SchemaLoader, SchemaOrigin } from './types';
import { loadFromSDL } from './sdl';
import { loadFromURL } from './url';

export { loadFromSDL, loadFromURL };

const getURLConfig = (origin: SchemaOrigin | null) => {
try {
return (
origin && {
url: new URL(typeof origin === 'object' ? origin.url : origin),
headers: typeof origin === 'object' ? origin.headers : undefined,
}
);
} catch (_error) {
throw new Error(`Configuration contains an invalid "schema" option`);
}
};

export interface LoadConfig {
origin: SchemaOrigin;
rootPath?: string;
fetchInterval?: number;
assumeValid?: boolean;
}

export function load(config: LoadConfig): SchemaLoader {
const urlOrigin = getURLConfig(origin);
if (urlOrigin) {
return loadFromURL({ ...urlOrigin, interval: config.fetchInterval });
} else if (typeof origin === 'string') {
const file = config.rootPath ? path.resolve(config.rootPath, origin) : origin;
const assumeValid = config.assumeValid != null ? config.assumeValid : true;
return loadFromSDL({ file, assumeValid });
} else {
throw new Error(`Configuration contains an invalid "schema" option`);
}
}
98 changes: 50 additions & 48 deletions packages/internal/src/loaders/sdl.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { IntrospectionQuery, GraphQLSchema } from 'graphql';
import type { IntrospectionQuery } from 'graphql';
import { buildSchema, buildClientSchema, executeSync } from 'graphql';
import { CombinedError } from '@urql/core';
import fs from 'node:fs/promises';
import path from 'node:path';

import type { SupportedFeatures } from './query';
import { makeIntrospectionQuery } from './query';
import type { SupportedFeatures } from './query';

import type { SchemaLoader } from './types';
import type { SchemaLoader, SchemaLoaderResult, OnSchemaUpdate } from './types';

interface LoadFromSDLConfig {
assumeValid?: boolean;
Expand All @@ -21,68 +21,62 @@ const ALL_SUPPORTED_FEATURES: SupportedFeatures = {
};

export function loadFromSDL(config: LoadFromSDLConfig): SchemaLoader {
const subscriptions = new Set<() => void>();
const subscriptions = new Set<OnSchemaUpdate>();

let controller: AbortController | null = null;
let introspection: IntrospectionQuery | null = null;
let schema: GraphQLSchema | null = null;
let result: SchemaLoaderResult | null = null;

const load = async (): Promise<typeof result> => {
const ext = path.extname(config.file);
const data = await fs.readFile(config.file, { encoding: 'utf8' });
if (ext === '.json') {
let introspection: IntrospectionQuery | null = null;
try {
introspection = JSON.parse(data);
} catch (_error) {}
return (
introspection && {
introspection,
schema: buildClientSchema(introspection, { assumeValid: !!config.assumeValid }),
}
);
} else {
const schema = buildSchema(data, { assumeValidSDL: !!config.assumeValid });
const query = makeIntrospectionQuery(ALL_SUPPORTED_FEATURES);
const queryResult = executeSync({ schema, document: query });
if (queryResult.errors) {
throw new CombinedError({ graphQLErrors: queryResult.errors as any[] });
} else if (queryResult.data) {
const introspection = queryResult.data as unknown as IntrospectionQuery;
return { introspection, schema };
} else {
return null;
}
}
};

const watch = async () => {
controller = new AbortController();
const watcher = fs.watch(config.file, {
persistent: false,
signal: controller.signal,
persistent: false,
});

try {
for await (const event of watcher) {
if (event.eventType === 'rename' || !subscriptions.size) break;
for (const subscriber of subscriptions) subscriber();
for await (const _event of watcher) {
if ((result = await load())) {
for (const subscriber of subscriptions) subscriber(result);
}
}
} catch (error: any) {
if (error.name === 'AbortError') {
return;
} else {
throw error;
}
if (error.name !== 'AbortError') throw error;
} finally {
controller = null;
}
};

const introspect = async () => {
const ext = path.extname(config.file);
const data = await fs.readFile(config.file, { encoding: 'utf8' });
if (ext === '.json') {
introspection = JSON.parse(data) || null;
schema =
introspection && buildClientSchema(introspection, { assumeValid: !!config.assumeValid });
return introspection;
} else {
schema = buildSchema(data, { assumeValidSDL: !!config.assumeValid });
const query = makeIntrospectionQuery(ALL_SUPPORTED_FEATURES);
const result = executeSync({ schema, document: query });
if (result.errors) {
throw new CombinedError({ graphQLErrors: result.errors as any[] });
} else if (result.data) {
return (introspection = (result.data as any) || null);
} else {
return (introspection = null);
}
}
};

return {
async loadIntrospection() {
return introspect();
},
async loadSchema() {
if (schema) {
return schema;
} else {
await this.loadIntrospection();
return schema;
}
async load(reload?: boolean) {
return reload || !result ? (result = await load()) : result;
},
notifyOnUpdate(onUpdate) {
if (!subscriptions.size) watch();
Expand All @@ -94,5 +88,13 @@ export function loadFromSDL(config: LoadFromSDLConfig): SchemaLoader {
}
};
},
async loadIntrospection() {
const result = await this.load();
return result && result.introspection;
},
async loadSchema() {
const result = await this.load();
return result && result.schema;
},
};
}
20 changes: 19 additions & 1 deletion packages/internal/src/loaders/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
import type { IntrospectionQuery, GraphQLSchema } from 'graphql';

export interface SchemaLoaderResult {
introspection: IntrospectionQuery;
schema: GraphQLSchema;
}

export type OnSchemaUpdate = (result: SchemaLoaderResult) => void;

export interface SchemaLoader {
load(reload?: boolean): Promise<SchemaLoaderResult | null>;
notifyOnUpdate(onUpdate: OnSchemaUpdate): () => void;

/** @internal */
loadIntrospection(): Promise<IntrospectionQuery | null>;
/** @internal */
loadSchema(): Promise<GraphQLSchema | null>;
notifyOnUpdate(onUpdate: () => void): () => void;
}

export type SchemaOrigin =
| string
| {
url: string;
headers?: HeadersInit;
};
Loading
Loading