Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions .changeset/eighty-poets-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@graphql-mesh/supergraph': minor
'@graphql-mesh/graphql': minor
'@omnigraph/json-schema': minor
'@omnigraph/openapi': minor
'json-machete': minor
'@graphql-mesh/utils': minor
---

Use `fetch` with `file://` to access files
5 changes: 5 additions & 0 deletions .changeset/lucky-otters-cough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-mesh/utils': minor
---

Use `URL.canParse` to check if it is URL
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('JSON Schema File Uploads', () => {
});
mesh = await getMesh({
...config,
fetchFn: router.fetch as any,
fetchFn: router.fetch,
});
});
afterAll(() => {
Expand Down
22 changes: 14 additions & 8 deletions examples/json-schema-subscriptions/api/app.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { createRouter, Response } from 'fets';
import { MeshFetch } from '@graphql-mesh/types';
import { fetch as defaultFetch } from '@whatwg-node/fetch';

export function createApi(fetch = defaultFetch) {
export function createApi(fetch: MeshFetch = defaultFetch) {
let todos = [];

const app = createRouter()
Expand All @@ -20,13 +21,18 @@ export function createApi(fetch = defaultFetch) {
...reqBody,
};
todos.push(todo);
await fetch('http://127.0.0.1:4000/webhooks/todo_added', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(todo),
}).catch(console.log);
try {
const res = await fetch('http://127.0.0.1:4000/webhooks/todo_added', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(todo),
});
console.log('Webhook response', await res.text());
} catch (e) {
console.error('Failed to send webhook', e);
}
return Response.json(todo);
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe('JSON Schema Subscriptions', () => {
baseDir,
getBuiltMesh: () => fakePromise(mesh),
});
const api = createApi(meshHttp.fetch as any);
const api = createApi(meshHttp.fetch);
});
afterEach(() => {
resetTodos();
Expand Down
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,14 @@
},
"resolutions": {
"@changesets/assemble-release-plan": "patch:@changesets/assemble-release-plan@npm%3A5.2.3#~/.yarn/patches/@changesets-assemble-release-plan-npm-5.2.3-296454a28f.patch",
"@graphql-tools/graphql-file-loader": "8.0.13-alpha-20250124175511-cf926757b2731edb762be661f8671ed41d778b2f",
"@graphql-tools/json-file-loader": "8.0.12-alpha-20250124175511-cf926757b2731edb762be661f8671ed41d778b2f",
"@graphql-tools/load": "8.0.13-alpha-20250124175511-cf926757b2731edb762be661f8671ed41d778b2f",
"@graphql-tools/merge": "9.0.18-alpha-20250124175511-cf926757b2731edb762be661f8671ed41d778b2f",
"@graphql-tools/url-loader": "8.0.25-alpha-20250124175511-cf926757b2731edb762be661f8671ed41d778b2f",
"@graphql-tools/utils": "10.8.0-alpha-20250124175511-cf926757b2731edb762be661f8671ed41d778b2f",
"@whatwg-node/fetch": "0.10.3",
"@whatwg-node/node-fetch": "0.7.7",
"@whatwg-node/node-fetch": "0.7.8-alpha-20250124174946-bf76ab63fc7e7c5ee6898f3bf12f95ef0ac79a63",
"@whatwg-node/server": "0.9.65",
"axios": "^1.0.0",
"cross-fetch": "^4.0.0",
Expand Down
10 changes: 8 additions & 2 deletions packages/json-machete/src/dereferenceObject.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import JsonPointer from 'json-pointer';
import urlJoin from 'url-join';
import type { MeshFetch } from '@graphql-mesh/types';
import { handleUntitledDefinitions } from './healUntitledDefinitions.js';

export const resolvePath = (path: string, root: any): any => {
Expand Down Expand Up @@ -61,6 +62,7 @@ export async function dereferenceObject<T extends object, TRoot = T>(
obj: T,
{
cwd = globalThis?.process.cwd(),
fetch,
externalFileCache = new Map<string, any>(),
refMap = new Map<string, any>(),
root = obj as any,
Expand All @@ -69,11 +71,12 @@ export async function dereferenceObject<T extends object, TRoot = T>(
resolvedObjects = new WeakSet(),
}: {
cwd?: string;
fetch: MeshFetch;
externalFileCache?: Map<string, any>;
refMap?: Map<string, any>;
root?: TRoot;
debugLogFn?(message?: any): void;
readFileOrUrl(path: string, opts: { cwd: string }): Promise<any> | any;
readFileOrUrl(path: string, opts: { cwd: string; fetch: MeshFetch }): Promise<any> | any;
resolvedObjects?: WeakSet<any>;
},
): Promise<T> {
Expand All @@ -91,7 +94,7 @@ export async function dereferenceObject<T extends object, TRoot = T>(
let externalFile = externalFileCache.get(externalFilePath);
if (!externalFile) {
try {
externalFile = await readFileOrUrl(externalFilePath, { cwd });
externalFile = await readFileOrUrl(externalFilePath, { cwd, fetch });
} catch (e) {
console.error(e);
throw new Error(`Unable to load ${externalRelativeFilePath} from ${cwd}`);
Expand Down Expand Up @@ -137,6 +140,7 @@ export async function dereferenceObject<T extends object, TRoot = T>(
readFileOrUrl,
root: externalFile,
resolvedObjects,
fetch,
},
);
refMap.set($ref, result);
Expand All @@ -161,6 +165,7 @@ export async function dereferenceObject<T extends object, TRoot = T>(
*/
const result = await dereferenceObject(resolvedObj, {
cwd,
fetch,
externalFileCache,
refMap,
root,
Expand Down Expand Up @@ -193,6 +198,7 @@ export async function dereferenceObject<T extends object, TRoot = T>(
debugLogFn,
readFileOrUrl,
resolvedObjects,
fetch,
});
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/legacy/handlers/graphql/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,14 +263,14 @@ export default class GraphQLHandler implements MeshHandler {
source: schemaConfig,
}: YamlConfig.GraphQLHandlerCodeFirstConfiguration): Promise<MeshSource> {
if (schemaConfig.endsWith('.graphql')) {
const rawSDL = await readFileOrUrl<string>(schemaConfig, {
const rawAst = await readFileOrUrl<DocumentNode>(schemaConfig, {
cwd: this.baseDir,
allowUnknownExtensions: true,
importFn: this.importFn,
fetch: this.fetchFn,
logger: this.logger,
});
const schema = buildSchema(rawSDL, {
const schema = buildASTSchema(rawAst, {
assumeValid: true,
assumeValidSDL: true,
});
Expand Down
8 changes: 6 additions & 2 deletions packages/legacy/handlers/supergraph/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ export default class SupergraphHandler implements MeshHandler {
fetch: this.fetchFn,
logger: this.logger,
}).catch(e => {
throw new Error(`Failed to load supergraph SDL from ${interpolatedSource}:\n ${e.message}`);
throw new Error(
`Supergraph source must be a valid GraphQL SDL string or a parsed DocumentNode, but got an invalid result from ${interpolatedSource} instead.\n Got error: ${e.message}`,
);
});
return handleSupergraphResponse(res, interpolatedSource);
}
Expand All @@ -81,7 +83,9 @@ export default class SupergraphHandler implements MeshHandler {
fetch: this.fetchFn,
logger: this.logger,
}).catch(e => {
throw new Error(`Failed to load supergraph SDL from ${interpolatedSource}:\n ${e.message}`);
throw new Error(
`Supergraph source must be a valid GraphQL SDL string or a parsed DocumentNode, but got an invalid result from ${interpolatedSource} instead.\n Got error: ${e.message}`,
);
});
return handleSupergraphResponse(sdlOrIntrospection, interpolatedSource);
});
Expand Down
6 changes: 2 additions & 4 deletions packages/legacy/handlers/supergraph/tests/supergraph.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,6 @@ describe('Supergraph', () => {
expect(logger.error.mock.calls[0][0].toString())
.toBe(`Failed to generate the schema for the source
Supergraph source must be a valid GraphQL SDL string or a parsed DocumentNode, but got an invalid result from ./fixtures/supergraph-invalid.graphql instead.
Got result: type Query {

Got error: Syntax Error: Expected Name, found <EOF>.`);
});
it('throws a helpful error when the source is down', async () => {
Expand All @@ -270,8 +268,8 @@ describe('Supergraph', () => {
).rejects.toThrow();
expect(logger.error.mock.calls[0][0].toString())
.toBe(`Failed to generate the schema for the source
Failed to load supergraph SDL from http://down-sdl-source.com/my-sdl.graphql:
getaddrinfo ENOTFOUND down-sdl-source.com`);
Supergraph source must be a valid GraphQL SDL string or a parsed DocumentNode, but got an invalid result from http://down-sdl-source.com/my-sdl.graphql instead.
Got error: getaddrinfo ENOTFOUND down-sdl-source.com`);
});
it('configures WebSockets for subscriptions correctly', async () => {
await using authorsHttpServer = await createDisposableServer(authorsServer);
Expand Down
2 changes: 1 addition & 1 deletion packages/legacy/testing/getTestMesh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function getTestMesh(extraOptions?: Partial<GetMeshOptions>) {
validate: false,
});
return getMesh({
fetchFn: yoga.fetch as any,
fetchFn: yoga.fetch,
sources: [
{
name: 'Yoga',
Expand Down
113 changes: 74 additions & 39 deletions packages/legacy/utils/src/read-file-or-url.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { parse, Source } from 'graphql';
import type { Schema } from 'js-yaml';
import { DEFAULT_SCHEMA, load as loadYamlFromJsYaml, Type } from 'js-yaml';
import { fs, path as pathModule } from '@graphql-mesh/cross-helpers';
import type { ImportFn, Logger, MeshFetch, MeshFetchRequestInit } from '@graphql-mesh/types';
import { fetch } from '@whatwg-node/fetch';
import { isUrl, isValidPath, mapMaybePromise } from '@graphql-tools/utils';
import { fetch as defaultFetch } from '@whatwg-node/fetch';
import { loadFromModuleExportExpression } from './load-from-module-export-expression.js';

export interface ReadFileOrUrlOptions extends MeshFetchRequestInit {
allowUnknownExtensions?: boolean;
fallbackFormat?: 'json' | 'yaml' | 'js' | 'ts';
fallbackFormat?: 'json' | 'yaml' | 'js' | 'ts' | 'graphql';
cwd: string;
fetch: MeshFetch;
importFn: ImportFn;
logger: Logger;
}

export function isUrl(str: string): boolean {
return /^https?:\/\//.test(str);
}
export { isUrl };

export async function readFileOrUrl<T>(
filePathOrUrl: string,
Expand All @@ -25,11 +25,17 @@ export async function readFileOrUrl<T>(
if (isUrl(filePathOrUrl)) {
config.logger.debug(`Fetching ${filePathOrUrl} via HTTP`);
return readUrl(filePathOrUrl, config);
} else if (filePathOrUrl.startsWith('{') || filePathOrUrl.startsWith('[')) {
} else if (
filePathOrUrl.startsWith('{') ||
filePathOrUrl.startsWith('[') ||
filePathOrUrl.startsWith('"')
) {
return JSON.parse(filePathOrUrl);
} else {
} else if (isValidPath(filePathOrUrl)) {
config.logger.debug(`Reading ${filePathOrUrl} from the file system`);
return readFile(filePathOrUrl, config);
} else {
return filePathOrUrl as T;
}
}

Expand Down Expand Up @@ -80,48 +86,77 @@ export function loadYaml(filepath: string, content: string, logger: Logger): any
});
}

export async function readFile<T>(
function isAbsolute(path: string): boolean {
return path.startsWith('/') || /^[A-Z]:\\/i.test(path);
}

export function readFile<T>(
fileExpression: string,
{ allowUnknownExtensions, cwd, fallbackFormat, importFn, logger }: ReadFileOrUrlOptions,
{
allowUnknownExtensions,
cwd,
fallbackFormat,
importFn,
logger,
fetch = defaultFetch,
}: ReadFileOrUrlOptions,
): Promise<T> {
const [filePath] = fileExpression.split('#');
if (/js$/.test(filePath) || /ts$/.test(filePath)) {
return loadFromModuleExportExpression<T>(fileExpression, {
cwd,
importFn,
defaultExportName: 'default',
});
}
const actualPath = pathModule.isAbsolute(filePath) ? filePath : pathModule.join(cwd, filePath);
const rawResult = await fs.promises.readFile(actualPath, 'utf-8');
if (/json$/.test(actualPath)) {
return JSON.parse(rawResult);
}
if (/yaml$/.test(actualPath) || /yml$/.test(actualPath)) {
return loadYaml(actualPath, rawResult, logger);
} else if (fallbackFormat) {
switch (fallbackFormat) {
case 'json':
return JSON.parse(rawResult);
case 'yaml':
return loadYaml(actualPath, rawResult, logger);
case 'ts':
case 'js':
return importFn(actualPath);
}
} else if (!allowUnknownExtensions) {
throw new Error(
`Failed to parse JSON/YAML. Ensure file '${filePath}' has ` +
`the correct extension (i.e. '.json', '.yaml', or '.yml).`,
if (/js$/.test(filePath) || /ts$/.test(filePath) || /json$/.test(filePath)) {
return mapMaybePromise(
loadFromModuleExportExpression<T>(fileExpression, {
cwd,
importFn,
defaultExportName: 'default',
}),
res => JSON.parse(JSON.stringify(res)),
);
}
return rawResult as unknown as T;
const actualPath = isAbsolute(filePath) ? filePath : `${cwd}/${filePath}`;
const url = `file://${actualPath}`;
return mapMaybePromise(fetch(url), res => {
if (/json$/.test(actualPath) || res.headers.get('content-type')?.includes('json')) {
return res.json();
}
return mapMaybePromise(res.text(), rawResult => {
if (/yaml$/.test(actualPath) || /yml$/.test(actualPath)) {
return loadYaml(actualPath, rawResult, logger);
} else if (
/graphql$/.test(actualPath) ||
/graphqls$/.test(actualPath) ||
/gql$/.test(actualPath) ||
/gqls$/.test(actualPath) ||
res.headers.get('content-type')?.includes('graphql')
) {
const source = new Source(rawResult, actualPath);
return parse(source);
} else if (fallbackFormat) {
switch (fallbackFormat) {
case 'json':
return JSON.parse(rawResult);
case 'yaml':
return loadYaml(actualPath, rawResult, logger);
case 'ts':
case 'js':
return importFn(actualPath);
case 'graphql':
return parse(new Source(rawResult, actualPath));
}
} else if (!allowUnknownExtensions) {
throw new Error(
`Failed to parse JSON/YAML. Ensure file '${filePath}' has ` +
`the correct extension (i.e. '.json', '.yaml', or '.yml).`,
);
}
return rawResult as unknown as T;
});
});
}

export async function readUrl<T>(path: string, config: ReadFileOrUrlOptions): Promise<T> {
const { allowUnknownExtensions, fallbackFormat } = config || {};
config.headers ||= {};
config.fetch ||= fetch;
config.fetch ||= defaultFetch;
const response = await config.fetch(path, config);
const contentType = response.headers?.get('content-type') || '';
const responseText = await response.text();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export async function getDereferencedJSONSchemaFromOperations({
});
const fullyDeferencedSchema = await dereferenceObject(referencedJSONSchema, {
cwd,
fetch: fetchFn,
debugLogFn: dereferenceObjectLogger.debug.bind(dereferenceObjectLogger),
readFileOrUrl: readFileOrUrlForJsonMachete,
});
Expand Down
Loading
Loading