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

feat(get-schema): allow all or multiple projects and endpoints #93

Merged
merged 2 commits into from
Jan 3, 2018
Merged
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
230 changes: 152 additions & 78 deletions src/cmds/get-schema.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { EventEmitter } from 'events'
import * as fs from 'fs'
import { relative } from 'path'
import { printSchema, GraphQLSchema } from 'graphql'
import { writeSchema } from 'graphql-config'
import { writeSchema, GraphQLConfig, GraphQLProjectConfig, GraphQLEndpoint } from 'graphql-config'
import chalk from 'chalk'

import { Context, noEndpointError, CommandObject } from '..'
import { Arguments } from 'yargs'
import { merge } from 'lodash'

const emitter = new EventEmitter()
let log: (text) => void
let start: (text?) => void

const command: CommandObject = {
command: 'get-schema',
Expand All @@ -17,101 +23,137 @@ const command: CommandObject = {
watch: {
alias: 'w',
boolean: true,
description:
'watch server for schema changes and update local schema',
description: 'watch server for schema changes and update local schema'
},
endpoint: {
alias: 'e',
describe: 'Endpoint name',
type: 'string',
type: 'string'
},
json: {
alias: 'j',
describe: 'Output as JSON',
type: 'boolean',
type: 'boolean'
},
output: {
alias: 'o',
describe: 'Output file name',
type: 'string',
type: 'string'
},
console: {
alias: 'c',
describe: 'Output to console',
default: false
},
insecure: {
alias: 'i',
describe: 'Allow insecure (self-signed) certificates',
type: 'boolean',
type: 'boolean'
},
all: {
describe: 'Get schema for all projects and all endpoints',
type: 'boolean'
}
})
.implies('console', ['--no-output', '--no-watch'])
.implies('--no-json', '--no-output'),
.implies('--no-json', '--no-output')
.implies('all', ['--no-output', '--no-endpoint', '--no-project']),

handler: async (context: Context, argv: Arguments) => {
if (argv.all) {
argv.project = argv.endpoint = '*'
}
const spinner = context.spinner

start = text => {
if (!argv.console) {
context.spinner.start(text)
}
}
log = text => {
if (!argv.console) {
context.spinner.text = text
}
}

if (argv.insecure) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
}

if (!argv.watch) {
return update(context, argv, console.log)
emitter.on('checked', () => {
spinner.stop()
if (!argv.console) { console.log(spinner.text) }
spinner.start()
})
emitter.on('error', err => {
spinner.fail(chalk.red(err.message))
})
start()
await updateWrapper(context, argv)
spinner.stop()
}

if (argv.watch) {
const spinner = context.spinner
// FIXME: stop spinner on errors
spinner.start()
const spinnerLog = msg => (spinner.text = msg)

while (true) {
try {
const isUpdated = await update(context, argv, spinnerLog)
if (isUpdated) {
spinner.stop()
console.log(spinner.text)
spinner.start()
spinner.text = 'Updated!'
} else {
spinner.text = 'No changes.'
}
} catch (err) {
spinner.stop()
console.error(chalk.red(err.message))
spinner.start()
spinner.text = 'Error.'
}
let handle
emitter.on('checked', () => {
spinner.stop()
console.log(`[${new Date().toTimeString().split(' ')[0]}] ${spinner.text}`)
spinner.start('Next update in 10s...')
})

spinner.text += ' Next update in 10s.'
await wait(10000)
}
emitter.on('error', err => {
spinner.fail(chalk.red(err.message))
clearInterval(handle)
})

updateWrapper(context, argv)
spinner.start()
handle = setInterval(updateWrapper, 10000, context, argv)
}
},
}
}

async function update(
context: Context,
argv: Arguments,
log: (message: string) => void,
) {
const config = await context.getProjectConfig()
if (!config.endpointsExtension) {
throw noEndpointError
async function updateWrapper(context: Context, argv: Arguments) {
try {
await update(context, argv)
} catch (err) {
emitter.emit('error', err)
}
const endpoint = config.endpointsExtension.getEndpoint(argv.endpoint)
}

if (!argv.console) {
log(`Downloading introspection from ${chalk.blue(endpoint.url)}`)
async function update(context: Context, argv: Arguments) {
const projects = await getProjectConfig(context, argv)

for (const projectName in projects) {
const config = projects[projectName]
if (!config.endpointsExtension) {
throw noEndpointError
}

const endpoints = getEndpoints(config, argv)
for (const endpointName in endpoints) {
const endpoint = endpoints[endpointName]
await updateSingleProjectEndpoint(config, endpoint, endpointName, argv)
}
}
}

async function updateSingleProjectEndpoint(
config: GraphQLProjectConfig,
endpoint: GraphQLEndpoint,
endpointName: string,
argv: Arguments
): Promise<void> {
log(`Downloading introspection from ${chalk.blue(endpoint.url)}`)
const newSchemaResult = argv.json
? await endpoint.resolveIntrospection()
: await endpoint.resolveSchema()

let oldSchema: string | undefined
if (!argv.console) {
let oldSchema: string | undefined
try {
oldSchema = argv.json
? fs.readFileSync(argv.output, 'utf-8')
: config.getSchemaSDL()
oldSchema = argv.json ? fs.readFileSync(argv.output, 'utf-8') : config.getSchemaSDL()
} catch (e) {
// ignore error if no previous schema file existed
if (e.code !== 'ENOENT') {
Expand All @@ -123,52 +165,84 @@ async function update(
? JSON.stringify(newSchemaResult, null, 2)
: printSchema(newSchemaResult as GraphQLSchema)
if (newSchema === oldSchema) {
log(chalk.green('No changes'))
return false
log(
chalk.green(
`No changes${config.projectName && config.projectName !== 'unnamed' ? ` - project ${chalk.white(config.projectName)}` : ''}${
endpointName && endpointName !== 'unnamed' ? ` - endpoint ${chalk.white(endpointName)}` : ''
}`
)
)
emitter.emit('checked')
return
}
}
}

const schemaPath = argv.json
? argv.output
: relative(process.cwd(), config.schemaPath as string)
const schemaPath = argv.json ? argv.output : relative(process.cwd(), config.schemaPath as string)
if (argv.console) {
console.log(
argv.json
? JSON.stringify(newSchemaResult, null, 2)
: printSchema(newSchemaResult as GraphQLSchema),
: printSchema(newSchemaResult as GraphQLSchema)
)
} else if (argv.json) {
fs.writeFileSync(schemaPath, JSON.stringify(newSchemaResult, null, 2))
} else {
await writeSchema(
config.schemaPath as string,
newSchemaResult as GraphQLSchema,
{
source: endpoint.url,
timestamp: new Date().toString(),
},
)
await writeSchema(config.schemaPath as string, newSchemaResult as GraphQLSchema, {
source: endpoint.url,
timestamp: new Date().toString()
})
}

if (!argv.console) {
const existed = fs.existsSync(schemaPath)
log(
chalk.green(
`Schema file was ${existed ? 'updated' : 'created'}: ${chalk.blue(
schemaPath,
)}`,
),
)
const existed = fs.existsSync(schemaPath)
log(chalk.green(`Schema file was ${existed ? 'updated' : 'created'}: ${chalk.blue(schemaPath)}`))
emitter.emit('checked')
}

async function getProjectConfig(context: Context, argv: Arguments): Promise<{ [name: string]: GraphQLProjectConfig }> {
const config: GraphQLConfig = await context.getConfig()
let projects: { [name: string]: GraphQLProjectConfig } | undefined
if (argv.project) {
if (Array.isArray(argv.project)) {
projects = {}
argv.project.map((p: string) => merge(projects, { [p]: config.getProjectConfig(p) }))
} else if (argv.project === '*') {
projects = config.getProjects()
} else {
// Single project mode
projects = { [argv.project]: config.getProjectConfig(argv.project) }
}
} else {
// Process all projects
projects = { unnamed: config.getProjectConfig() }
}

if (!projects) {
throw new Error('No projects defined in config file')
}

return true
return projects
}

function wait(interval: number): Promise<void> {
return new Promise(resolve => {
setTimeout(() => resolve(), interval)
})
function getEndpoints(config: GraphQLProjectConfig, argv: Arguments): { [name: string]: GraphQLEndpoint } {
let endpoints: { [name: string]: GraphQLEndpoint } | undefined
if (argv.endpoint) {
if (Array.isArray(argv.endpoint)) {
endpoints = {}
argv.endpoint.map((e: string) => merge(endpoints, { [e]: config.endpointsExtension!.getEndpoint(e) }))
} else if (argv.endpoint === '*') {
endpoints = Object.keys(config.endpointsExtension!.getRawEndpointsMap()).reduce((total, current) => {
merge(total, { [current]: config.endpointsExtension!.getEndpoint(current) })
return total
}, {})
} else {
endpoints = { [argv.endpoint]: config.endpointsExtension!.getEndpoint(argv.endpoint) }
}
} else {
endpoints = { unnamed: config.endpointsExtension!.getEndpoint(argv.endpoint) }
}

return endpoints
}

export = command