Skip to content

Commit

Permalink
[v1] JS config support and more defaults (#7257)
Browse files Browse the repository at this point in the history
  • Loading branch information
enisdenjo authored Jul 10, 2024
1 parent a321ad5 commit 798ed17
Show file tree
Hide file tree
Showing 11 changed files with 610 additions and 69 deletions.
7 changes: 7 additions & 0 deletions .changeset/heavy-lizards-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@graphql-mesh/compose-cli': minor
'@graphql-mesh/serve-cli': minor
---

Support mesh.config.ts or mesh.config.mts or mesh.config.cts or mesh.config.js or mesh.config.mjs or
mesh.config.cjs configuration files
89 changes: 89 additions & 0 deletions e2e/js-config/__snapshots__/js-config.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should compose and serve 1`] = `
"
schema
@link(url: "https://specs.apollo.dev/link/v1.0")
@link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
{
query: Query
}
directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
directive @join__field(
graph: join__Graph
requires: join__FieldSet
provides: join__FieldSet
type: String
external: Boolean
override: String
usedOverridden: Boolean
) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
directive @join__implements(
graph: join__Graph!
interface: String!
) repeatable on OBJECT | INTERFACE
directive @join__type(
graph: join__Graph!
key: join__FieldSet
extension: Boolean! = false
resolvable: Boolean! = true
isInterfaceObject: Boolean! = false
) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
scalar join__FieldSet
directive @link(
url: String
as: String
for: link__Purpose
import: [link__Import]
) repeatable on SCHEMA
scalar link__Import
enum link__Purpose {
"""
\`SECURITY\` features provide metadata necessary to securely resolve fields.
"""
SECURITY
"""
\`EXECUTION\` features provide metadata necessary for operation execution.
"""
EXECUTION
}
enum join__Graph {
HELLOWORLD @join__graph(name: "helloworld", url: "")
}
type Query @join__type(graph: HELLOWORLD) {
hello: String
}
"
`;
15 changes: 15 additions & 0 deletions e2e/js-config/js-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createTenv } from '@e2e/tenv';

const { serve, compose, fs } = createTenv(__dirname);

it('should compose and serve', async () => {
const { result: composedSchema } = await compose();
expect(composedSchema).toMatchSnapshot();

const supergraphPath = await fs.tempfile('supergraph.graphql');
await fs.write(supergraphPath, composedSchema);
const { port } = await serve({ supergraph: supergraphPath });
const res = await fetch(`http://0.0.0.0:${port}/graphql?query={hello}`);
expect(res.ok).toBeTruthy();
await expect(res.text()).resolves.toMatchInlineSnapshot(`"{"data":{"hello":"world"}}"`);
});
32 changes: 32 additions & 0 deletions e2e/js-config/mesh.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql';

export const composeConfig = {
subgraphs: [
{
sourceHandler: () => ({
name: 'helloworld',
schema$: new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
hello: {
type: GraphQLString,
resolve: () => 'world',
},
},
}),
}),
}),
},
],
};

export const serveConfig = {
additionalResolvers: {
Query: {
hello() {
return 'world';
},
},
},
};
10 changes: 10 additions & 0 deletions e2e/js-config/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "@e2e/js-config",
"type": "module",
"private": true,
"dependencies": {
"@graphql-mesh/compose-cli": "workspace:*",
"@graphql-mesh/serve-cli": "workspace:*",
"graphql": "16.9.0"
}
}
3 changes: 2 additions & 1 deletion e2e/sqlite-chinook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"@graphql-mesh/compose-cli": "workspace:*",
"@graphql-mesh/serve-cli": "workspace:*",
"@omnigraph/sqlite": "workspace:*",
"graphql": "^16.8.1"
"graphql": "^16.8.1",
"sqlite3": "^5.1.7"
}
}
79 changes: 54 additions & 25 deletions packages/compose-cli/src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,25 @@ import { parse } from 'graphql';
import { Command, Option } from '@commander-js/extra-typings';
import type { Logger } from '@graphql-mesh/types';
import { DefaultLogger } from '@graphql-mesh/utils';
import { printSchemaWithDirectives } from '@graphql-tools/utils';
import { getComposedSchemaFromConfig } from './getComposedSchemaFromConfig.js';
import type { MeshComposeCLIConfig } from './types.js';

/** Default config paths sorted by priority. */
const defaultConfigPaths = [
'mesh.config.ts',
'mesh.config.mts',
'mesh.config.cts',
'mesh.config.js',
'mesh.config.mjs',
'mesh.config.cjs',
];

let program = new Command()
.addOption(
new Option('-c, --config-path <path>', 'path to the configuration file')
.env('CONFIG_PATH')
.default('mesh.config.ts'),
new Option(
'-c, --config-path <path>',
`path to the configuration file. defaults to the following files respectively in the current working directory: ${defaultConfigPaths.join(', ')}`,
).env('CONFIG_PATH'),
)
.option('--subgraph <name>', 'name of the subgraph to compose')
.option('-o, --output <path>', 'path to the output file');
Expand All @@ -36,8 +46,6 @@ export interface RunOptions extends ReturnType<typeof program.opts> {
version?: string;
}

export type ImportedModule<T> = T | { default: T };

export async function run({
log: rootLog = new DefaultLogger(),
productName = 'Mesh Compose',
Expand All @@ -52,28 +60,32 @@ export async function run({

const log = rootLog.child(`🕸️ ${productName}`);

const configPath = isAbsolute(opts.configPath)
? opts.configPath
: resolve(process.cwd(), opts.configPath);
log.info(`Checking configuration at ${configPath}`);
const importedConfigModule: ImportedModule<{ composeConfig?: MeshComposeCLIConfig }> =
await import(configPath).catch(err => {
if (err.code === 'ERR_MODULE_NOT_FOUND') {
return {}; // no config is ok
}
log.error('Loading configuration failed!');
throw err;
});

let importedConfig: MeshComposeCLIConfig;
if ('default' in importedConfigModule) {
importedConfig = importedConfigModule.default.composeConfig;
} else if ('composeConfig' in importedConfigModule) {
importedConfig = importedConfigModule.composeConfig;
if (!opts.configPath) {
log.info(`Searching for default config files`);
for (const configPath of defaultConfigPaths) {
importedConfig = await importConfig(log, resolve(process.cwd(), configPath));
if (importedConfig) {
break;
}
}
if (!importedConfig) {
throw new Error(
`Cannot find default config file at ${defaultConfigPaths.join(' or ')} in the current working directory`,
);
}
} else {
throw new Error(`No configuration found at ${configPath}`);
// using user-provided config
const configPath = isAbsolute(opts.configPath)
? opts.configPath
: resolve(process.cwd(), opts.configPath);
log.info(`Loading config file at path ${configPath}`);
importedConfig = await importConfig(log, configPath);
if (!importedConfig) {
throw new Error(`Cannot find config file at ${configPath}`);
}
}
log.info('Loaded configuration');
log.info('Loaded config file');

const config: MeshComposeCLIConfig = {
...importedConfig,
Expand Down Expand Up @@ -124,3 +136,20 @@ export async function run({

log.info('Done!');
}

async function importConfig(log: Logger, path: string): Promise<MeshComposeCLIConfig | null> {
try {
const importedConfigModule = await import(path);
if ('default' in importedConfigModule) {
return importedConfigModule.default.composeConfig;
} else if ('composeConfig' in importedConfigModule) {
return importedConfigModule.composeConfig;
}
} catch (err) {
if (err.code !== 'ERR_MODULE_NOT_FOUND') {
log.error('Importing configuration failed!');
throw err;
}
}
return null;
}
79 changes: 53 additions & 26 deletions packages/serve-cli/src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ import { startuWebSocketsServer } from './uWebSockets.js';

const defaultFork = process.env.NODE_ENV === 'production' ? availableParallelism() : 1;

/** Default config paths sorted by priority. */
const defaultConfigPaths = [
'mesh.config.ts',
'mesh.config.mts',
'mesh.config.cts',
'mesh.config.js',
'mesh.config.mjs',
'mesh.config.cjs',
];

let program = new Command()
.addOption(
new Option(
Expand All @@ -38,9 +48,10 @@ let program = new Command()
.default(defaultFork),
)
.addOption(
new Option('-c, --config-path <path>', 'path to the configuration file')
.env('CONFIG_PATH')
.default('mesh.config.ts'),
new Option(
'-c, --config-path <path>',
`path to the configuration file. defaults to the following files respectively in the current working directory: ${defaultConfigPaths.join(', ')}`,
).env('CONFIG_PATH'),
)
.option(
'-h, --host <hostname>',
Expand Down Expand Up @@ -74,8 +85,6 @@ export interface RunOptions extends ReturnType<typeof program.opts> {
version?: string;
}

export type ImportedModule<T> = T | { default: T };

export async function run({
log: rootLog = new DefaultLogger(),
productName = 'Mesh',
Expand All @@ -91,29 +100,30 @@ export async function run({
cluster.worker?.id ? `🕸️ ${productName} Worker#${cluster.worker.id}` : `🕸️ ${productName}`,
);

const configPath = isAbsolute(opts.configPath)
? opts.configPath
: resolve(process.cwd(), opts.configPath);
log.info(`Checking configuration at ${configPath}`);
const importedConfigModule: ImportedModule<{ serveConfig?: MeshServeCLIConfig }> = await import(
configPath
).catch(err => {
if (err.code === 'ERR_MODULE_NOT_FOUND') {
return {}; // no config is ok
}
log.error('Loading configuration failed!');
throw err;
});
let importedConfig: MeshServeCLIConfig;
if ('default' in importedConfigModule) {
log.info('Loaded configuration');
importedConfig = importedConfigModule.default.serveConfig;
} else if ('serveConfig' in importedConfigModule) {
log.info('Loaded configuration');
importedConfig = importedConfigModule.serveConfig;
if (!opts.configPath) {
log.info(`Searching for default config files`);
for (const configPath of defaultConfigPaths) {
importedConfig = await importConfig(log, resolve(process.cwd(), configPath));
if (importedConfig) {
break;
}
}
} else {
importedConfig = {};
log.info('No configuration found');
// using user-provided config
const configPath = isAbsolute(opts.configPath)
? opts.configPath
: resolve(process.cwd(), opts.configPath);
log.info(`Loading config file at path ${configPath}`);
importedConfig = await importConfig(log, configPath);
if (!importedConfig) {
throw new Error(`Cannot find config file at ${configPath}`);
}
}
if (importedConfig) {
log.info('Loaded config file');
} else {
log.debug('No config file loaded, using defaults');
}

const config: MeshServeCLIConfig = {
Expand Down Expand Up @@ -242,3 +252,20 @@ export async function run({
});
terminateStack.use(server);
}

async function importConfig(log: Logger, path: string): Promise<MeshServeCLIConfig | null> {
try {
const importedConfigModule = await import(path);
if ('default' in importedConfigModule) {
return importedConfigModule.default.serveConfig;
} else if ('serveConfig' in importedConfigModule) {
return importedConfigModule.serveConfig;
}
} catch (err) {
if (err.code !== 'ERR_MODULE_NOT_FOUND') {
log.error('Importing configuration failed!');
throw err;
}
}
return null;
}
Loading

0 comments on commit 798ed17

Please sign in to comment.