Skip to content

Commit

Permalink
feat(get-schema): allow all or multiple projects and endpoints
Browse files Browse the repository at this point in the history
- fix: error on multiple `-p`
- fix: error on multiple `-e`
- feat: allow `-p *` and `-e *` to run get-schema on all projects and/or all endpoints
- improved console output and error handling

Closes Urigo#54
  • Loading branch information
kbrandwijk committed Jan 3, 2018
1 parent 9bfa041 commit 18e5b13
Showing 1 changed file with 144 additions and 78 deletions.
222 changes: 144 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,129 @@ 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'
}
})
.implies('console', ['--no-output', '--no-watch'])
.implies('--no-json', '--no-output'),

handler: async (context: Context, argv: Arguments) => {
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 +157,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

0 comments on commit 18e5b13

Please sign in to comment.