Skip to content

Commit

Permalink
[core] GraphQL: Allow deploying legacy GraphQL API generation
Browse files Browse the repository at this point in the history
  • Loading branch information
rexxars authored and saasen committed Feb 27, 2020
1 parent 844eb50 commit ef6c0ff
Show file tree
Hide file tree
Showing 18 changed files with 457 additions and 11 deletions.
108 changes: 97 additions & 11 deletions packages/@sanity/core/src/actions/graphql/deployApiAction.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
const {get} = require('lodash')
const debug = require('../../debug').default
const getUrlHeaders = require('../../util/getUrlHeaders')
const {tryInitializePluginConfigs} = require('../config/reinitializePluginConfigs')
const getSanitySchema = require('./getSanitySchema')
const extractFromSanitySchema = require('./extractFromSanitySchema')
const generateTypeQueries = require('./generateTypeQueries')
const generateTypeFilters = require('./generateTypeFilters')
const generateTypeSortings = require('./generateTypeSortings')
const SchemaError = require('./SchemaError')

const v1 = require('./v1')
const v2 = require('./v2')

const latestGeneration = 'v2'
const generations = {
v1,
v2
}

module.exports = async function deployApiActions(args, context) {
const {apiClient, workDir, output, prompt} = context
const {apiClient, workDir, output, prompt, chalk} = context

await tryInitializePluginConfigs({workDir, output, env: 'production'})

let spinner
const flags = args.extOptions
const {force, playground} = flags

Expand All @@ -21,6 +31,32 @@ module.exports = async function deployApiActions(args, context) {

const dataset = flags.dataset || client.config().dataset
const tag = flags.tag || 'default'
let generation = flags.generation
if (generation && !generations.hasOwnProperty(generation)) {
throw new Error(`Unknown API generation "${generation}"`)
}

spinner = output.spinner('Checking for deployed API').start()
const currentGeneration = await getUrlHeaders(client.getUrl(`/apis/graphql/${dataset}/${tag}`), {
Authorization: `Bearer ${client.config().token}`
})
.then(res => res['x-sanity-graphql-generation'])
.catch(err => {
if (err.statusCode === 404) {
return null
}

throw err
})

spinner.succeed()

generation = await resolveApiGeneration({currentGeneration, flags, output, prompt, chalk})
if (!generation) {
// User cancelled
return
}

const enablePlayground =
typeof playground === 'undefined'
? await prompt.single({
Expand All @@ -30,17 +66,15 @@ module.exports = async function deployApiActions(args, context) {
})
: playground

let spinner = output.spinner('Generating GraphQL schema').start()
spinner = output.spinner('Generating GraphQL schema').start()

let schema
try {
const generateSchema = generations[generation]
const sanitySchema = getSanitySchema(workDir)
const extracted = extractFromSanitySchema(sanitySchema)
const filters = generateTypeFilters(extracted.types)
const sortings = generateTypeSortings(extracted.types)
const queries = generateTypeQueries(extracted.types, filters, sortings)
const types = extracted.types.concat(filters).concat(sortings)
schema = {types, queries, interfaces: extracted.interfaces, generation: 'v2'}

schema = generateSchema(extracted)
} catch (err) {
spinner.fail()

Expand All @@ -64,8 +98,9 @@ module.exports = async function deployApiActions(args, context) {
maxRedirects: 0
})
} catch (err) {
const validationError = get(err, 'response.body.validationError')
spinner.fail()
throw err
throw validationError ? new Error(validationError) : err
}

if (!(await confirmValidationResult(valid, {spinner, output, prompt, force}))) {
Expand Down Expand Up @@ -130,3 +165,54 @@ async function confirmValidationResult(valid, {spinner, output, prompt, force})

return shouldDeploy
}

async function resolveApiGeneration({currentGeneration, flags, output, prompt, chalk}) {
// a) If no API is currently disabled:
// use the specificed one, or use whichever generation is the latest
// b) If an API generation is specified explicitly:
// use the given one, but _prompt_ if it differs from the current one
// c) If no API generation is specified explicitly:
// use whichever is already deployed, but warn if differs from latest
if (!currentGeneration) {
const generation = flags.generation || latestGeneration
debug(
'There is no current generation deployed, using %s (%s)',
generation,
flags.generation ? 'specified' : 'default'
)
return generation
}

if (flags.generation && flags.generation !== currentGeneration) {
output.warn(
`Specified generation (${flags.generation}) differs from the one currently deployed (${currentGeneration}).`
)

const confirmDeploy =
flags.force ||
(await prompt.single({
type: 'confirm',
message: 'Are you sure you want to deploy?',
default: false
}))

return confirmDeploy ? flags.generation : null
}

const generation = flags.generation || currentGeneration
if (generation !== latestGeneration) {
output.warn(
chalk.cyan(
`A new generation of the GraphQL API is available, use \`--generation ${latestGeneration}\` to use it`
)
)
}

if (flags.generation) {
debug('Using specified (%s) generation', flags.generation)
return flags.generation
}

debug('Using the currently deployed version (%s)', currentGeneration)
return currentGeneration
}
225 changes: 225 additions & 0 deletions packages/@sanity/core/src/actions/graphql/v1/generateTypeFilters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
const {flatten} = require('lodash')

const filterCreators = {
ID: createIdFilters,
String: createStringFilters,
Url: createStringFilters,
Float: createNumberFilters,
Integer: createNumberFilters,
Boolean: createBooleanFilters,
Datetime: createDateFilters,
Date: createDateFilters,
Object: createObjectFilters
}

function generateTypeFilters(types) {
const queryable = types.filter(
type => type.type === 'Object' && type.interfaces && type.interfaces.includes('Document')
)

return queryable.map(type => {
const name = `${type.name}Filter`
const fields = flatten(type.fields.map(createFieldFilters)).filter(Boolean)
return {name, kind: 'InputObject', fields: fields.concat(getDocumentFilters(type))}
})
}

function createFieldFilters(field) {
if (filterCreators[field.type]) {
return filterCreators[field.type](field)
}

if (field.kind === 'List') {
return createListFilters(field)
}

if (field.isReference) {
return createReferenceFilters(field)
}

return createInlineTypeFilters(field)
}

function getFieldName(field, modifier = '') {
const suffix = modifier ? `_${modifier}` : ''
return `${field.fieldName}${suffix}`
}

function getDocumentFilters(type) {
return (
[
{
fieldName: 'references',
type: 'ID',
description: 'All documents references the given document ID',
constraint: {
comparator: 'REFERENCES'
}
}
],
{
fieldName: 'is_draft',
type: 'Boolean',
description: 'All documents that are drafts',
constraint: {
field: '_id',
comparator: 'IS_DRAFT'
}
}
)
}

function createEqualityFilter(field) {
return {
fieldName: getFieldName(field),
type: field.type,
description: 'All documents that are equal to given value',
constraint: {
field: field.fieldName,
comparator: 'EQUALS'
}
}
}

function createInequalityFilter(field) {
return {
fieldName: getFieldName(field, 'not'),
type: field.type,
description: 'All documents that are not equal to given value',
constraint: {
field: field.fieldName,
comparator: 'NOT_EQUALS'
}
}
}

function createDefaultFilters(field) {
return [createEqualityFilter(field), createInequalityFilter(field)]
}

function createGtLtFilters(field) {
return [
{
fieldName: getFieldName(field, 'lt'),
type: field.type,
description: 'All documents are less than given value',
constraint: {
field: field.fieldName,
comparator: 'LT'
}
},
{
fieldName: getFieldName(field, 'lte'),
type: field.type,
description: 'All documents are less than or equal to given value',
constraint: {
field: field.fieldName,
comparator: 'LTE'
}
},
{
fieldName: getFieldName(field, 'gt'),
type: field.type,
description: 'All documents are greater than given value',
constraint: {
field: field.fieldName,
comparator: 'GT'
}
},
{
fieldName: getFieldName(field, 'gte'),
type: field.type,
description: 'All documents are greater than or equal to given value',
constraint: {
field: field.fieldName,
comparator: 'GTE'
}
}
]
}

function createBooleanFilters(field) {
return createDefaultFilters(field)
}

function createIdFilters(field) {
return createStringFilters(field)
}

function createDateFilters(field) {
return createDefaultFilters(field).concat(createGtLtFilters(field))
}

function createStringFilters(field) {
return createDefaultFilters(field).concat([
{
fieldName: getFieldName(field, 'matches'),
type: 'String',
description: 'All documents contain (match) the given word/words',
constraint: {
field: field.fieldName,
comparator: 'MATCHES'
}
},
{
fieldName: getFieldName(field, 'in'),
kind: 'List',
children: {
type: 'String',
isNullable: false
},
description: 'All documents match one of the given values',
constraint: {
field: field.fieldName,
comparator: 'IN'
}
},
{
fieldName: getFieldName(field, 'not_in'),
kind: 'List',
children: {
type: 'String',
isNullable: false
},
description: 'None of the values match any of the given values',
constraint: {
field: field.fieldName,
comparator: 'NOT_IN'
}
}
])
}

function createNumberFilters(field) {
return createDefaultFilters(field).concat(createGtLtFilters(field))
}

function createObjectFilters(field) {
// @todo
return []
}

function createListFilters(field) {
// @todo
return []
}

function createInlineTypeFilters(field) {
// @todo
return []
}

function createReferenceFilters(field) {
return [
{
fieldName: getFieldName(field),
type: 'ID',
constraint: {
field: `${field.fieldName}._ref`,
comparator: 'EQUALS'
}
}
]
}

module.exports = generateTypeFilters

0 comments on commit ef6c0ff

Please sign in to comment.