@omnigraph/raml
Version:
This package generates `GraphQLSchema` instance from **RAML API Document** (`.raml`) file located at a URL or FileSystem by resolving the JSON Schema dependencies. It uses `@omnigraph/json-schema` by generating the necessary configuration.
371 lines (362 loc) • 17.3 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
const jsonSchema = require('@omnigraph/json-schema');
const utils = require('@graphql-mesh/utils');
const jsonMachete = require('json-machete');
const raml1Parser = require('@ardatan/raml-1-parser');
const fetch = require('@whatwg-node/fetch');
const toJsonSchema = _interopDefault(require('to-json-schema'));
const utils$1 = require('@graphql-tools/utils');
const changeCase = require('change-case');
const graphql = require('graphql');
const stringInterpolation = require('@graphql-mesh/string-interpolation');
const crossHelpers = require('@graphql-mesh/cross-helpers');
function getFieldNameFromPath(path, method, typeName) {
// Replace identifiers with "by"
path = path.split('{').join('by_').split('}').join('');
const [actualPartsStr, allQueryPartsStr] = path.split('?');
const actualParts = actualPartsStr.split('/').filter(Boolean);
let fieldNameWithoutMethod = actualParts.join('_');
// If path doesn't give any field name without identifiers, we can use the return type with HTTP Method name
if ((!fieldNameWithoutMethod || fieldNameWithoutMethod.startsWith('by')) && typeName) {
// lowercase looks better in the schema
const prefix = changeCase.camelCase(typeName);
if (fieldNameWithoutMethod) {
fieldNameWithoutMethod = prefix + '_' + fieldNameWithoutMethod;
}
else {
fieldNameWithoutMethod = prefix;
}
}
if (allQueryPartsStr) {
const queryParts = allQueryPartsStr.split('&');
for (const queryPart of queryParts) {
const [queryName] = queryPart.split('=');
fieldNameWithoutMethod += '_' + 'by' + '_' + queryName;
}
}
// get_ doesn't look good in field names
const methodPrefix = method.toLowerCase();
if (methodPrefix === 'get') {
return fieldNameWithoutMethod;
}
return methodPrefix + '_' + fieldNameWithoutMethod;
}
function resolveTraitsByIs(base) {
const allTraits = [];
for (const traitRef of base.is()) {
const traitNode = traitRef.trait();
if (traitNode) {
allTraits.push(traitNode);
allTraits.push(...resolveTraitsByIs(traitNode));
}
}
return allTraits;
}
/**
* Generates the options for JSON Schema Loader
* from RAML Loader options by extracting the JSON Schema references
* from RAML API Document
*/
async function getJSONSchemaOptionsFromRAMLOptions({ ramlFilePath, cwd: ramlFileCwd = crossHelpers.process.cwd(), operations: extraOperations, baseUrl: forcedBaseUrl, fetch: fetch$1 = fetch.fetch, schemaHeaders = {}, selectQueryOrMutationField = [], }) {
var _a, _b, _c, _d, _e, _f, _g;
const fieldTypeMap = {};
for (const { fieldName, type } of selectQueryOrMutationField) {
fieldTypeMap[fieldName] = type;
}
const operations = extraOperations || [];
const ramlAbsolutePath = jsonMachete.getAbsolutePath(ramlFilePath, ramlFileCwd);
const schemaHeadersFactory = stringInterpolation.getInterpolatedHeadersFactory(schemaHeaders);
const ramlAPI = (await raml1Parser.loadApi(ramlAbsolutePath, [], {
httpResolver: {
getResourceAsync: async (url) => {
const fetchResponse = await fetch$1(url, {
headers: schemaHeadersFactory({ env: crossHelpers.process.env }),
});
const content = await fetchResponse.text();
if (fetchResponse.status !== 200) {
return {
errorMessage: content,
};
}
return {
content,
};
},
getResource: () => {
throw new Error(`Sync fetching not available for URLs`);
},
},
}));
let baseUrl = forcedBaseUrl;
if (!baseUrl) {
baseUrl = ramlAPI.baseUri().value();
for (const baseUrlParamNode of ramlAPI.baseUriParameters()) {
const paramName = baseUrlParamNode.name();
baseUrl = baseUrl.split(`{${paramName}}`).join(`{context.${paramName}}`);
}
}
const pathTypeMap = new Map();
const typePathMap = new Map();
for (const typeNode of ramlAPI.types()) {
const typeNodeJson = typeNode.toJSON();
for (const typeName in typeNodeJson) {
const { schemaPath } = typeNodeJson[typeName];
if (schemaPath) {
pathTypeMap.set(schemaPath, typeName);
typePathMap.set(typeName, schemaPath);
}
}
}
const cwd = jsonMachete.getCwd(ramlAbsolutePath);
const apiQueryParameters = [];
const apiBodyNodes = [];
const apiResponses = [];
for (const traitNode of ramlAPI.traits()) {
apiQueryParameters.push(...traitNode.queryParameters());
apiBodyNodes.push(...traitNode.body());
apiResponses.push(...traitNode.responses());
const nestedTraits = resolveTraitsByIs(traitNode);
for (const nestedTrait of nestedTraits) {
apiQueryParameters.push(...nestedTrait.queryParameters());
apiBodyNodes.push(...nestedTrait.body());
apiResponses.push(...nestedTrait.responses());
}
}
for (const resourceNode of ramlAPI.allResources()) {
const resourceQueryParameters = [...apiQueryParameters];
const resourceBodyNodes = [...apiBodyNodes];
const resourceResponses = [...apiResponses];
const resourceTraits = resolveTraitsByIs(resourceNode);
for (const traitNode of resourceTraits) {
apiQueryParameters.push(...traitNode.queryParameters());
apiBodyNodes.push(...traitNode.body());
apiResponses.push(...traitNode.responses());
}
for (const methodNode of resourceNode.methods()) {
const queryParameters = [...resourceQueryParameters];
const bodyNodes = [...resourceBodyNodes];
const responses = [...resourceResponses];
const traits = resolveTraitsByIs(methodNode);
for (const traitNode of traits) {
queryParameters.push(...traitNode.queryParameters());
bodyNodes.push(...traitNode.body());
responses.push(...traitNode.responses());
}
queryParameters.push(...methodNode.queryParameters());
bodyNodes.push(...methodNode.body());
responses.push(...methodNode.responses());
let requestSchema;
let requestTypeName;
const responseByStatusCode = {};
const method = methodNode.method().toUpperCase();
let fieldName = (_a = methodNode.displayName()) === null || _a === void 0 ? void 0 : _a.replace('GET_', '');
const description = ((_b = methodNode.description()) === null || _b === void 0 ? void 0 : _b.value()) || ((_c = resourceNode.description()) === null || _c === void 0 ? void 0 : _c.value());
const originalFullRelativeUrl = resourceNode.completeRelativeUri();
let fullRelativeUrl = originalFullRelativeUrl;
const argTypeMap = {};
for (const uriParameterNode of resourceNode.uriParameters()) {
const paramName = uriParameterNode.name();
fullRelativeUrl = fullRelativeUrl.replace(`{${paramName}}`, `{args.${paramName}}`);
const uriParameterNodeJson = uriParameterNode.toJSON();
for (const typeName of utils$1.asArray(uriParameterNodeJson.type)) {
switch (typeName) {
case 'number':
argTypeMap[paramName] = 'Float';
break;
case 'boolean':
argTypeMap[paramName] = 'Boolean';
break;
case 'integer':
argTypeMap[paramName] = 'Int';
break;
default:
argTypeMap[paramName] = 'String';
break;
}
}
/* raml pattern is different
if (uriParameterNodeJson.pattern) {
const typeName = sanitizeNameForGraphQL(uriParameterNodeJson.displayName || `${fieldName}_${paramName}`);
argTypeMap[paramName] = new RegularExpression(typeName, new RegExp(uriParameterNodeJson.pattern), {
description: uriParameterNodeJson.description,
});
}
*/
if (uriParameterNodeJson.enum) {
const typeName = utils.sanitizeNameForGraphQL(uriParameterNodeJson.displayName || `${fieldName}_${paramName}`);
const values = {};
for (const value of utils$1.asArray(uriParameterNodeJson.enum)) {
let enumKey = utils.sanitizeNameForGraphQL(value.toString());
if (enumKey === 'false' || enumKey === 'true' || enumKey === 'null') {
enumKey = enumKey.toUpperCase();
}
if (typeof enumKey === 'string' && enumKey.length === 0) {
enumKey = '_';
}
values[enumKey] = {
// Falsy values are ignored by GraphQL
// eslint-disable-next-line no-unneeded-ternary
value: value ? value : value === null || value === void 0 ? void 0 : value.toString(),
};
}
argTypeMap[paramName] = new graphql.GraphQLEnumType({
name: typeName,
description: uriParameterNodeJson.description,
values,
});
}
if (uriParameterNodeJson.required) {
argTypeMap[paramName] += '!';
}
}
for (const queryParameterNode of queryParameters) {
requestSchema = requestSchema || {
type: 'object',
properties: {},
required: [],
};
const parameterName = queryParameterNode.name();
const queryParameterNodeJson = queryParameterNode.toJSON();
if (queryParameterNodeJson.required) {
requestSchema.required.push(parameterName);
}
if (queryParameterNodeJson.enum) {
requestSchema.properties[parameterName] = {
type: 'string',
enum: queryParameterNodeJson.enum,
};
}
if (queryParameterNodeJson.type) {
requestSchema.properties[parameterName] = {
type: utils$1.asArray(queryParameterNodeJson.type)[0] || 'string',
};
}
else {
requestSchema.properties[parameterName] = toJsonSchema((_d = queryParameterNodeJson.example) !== null && _d !== void 0 ? _d : queryParameterNodeJson.default, {
required: false,
strings: {
detectFormat: true,
},
});
}
if (queryParameterNodeJson.displayName) {
requestSchema.properties[parameterName].title = queryParameterNodeJson.displayName;
}
if (queryParameterNode.description) {
requestSchema.properties[parameterName].description =
queryParameterNodeJson.description;
}
}
for (const bodyNode of bodyNodes) {
if (bodyNode.name().includes('application/json')) {
const bodyJson = bodyNode.toJSON();
if (bodyJson.schemaPath) {
const schemaPath = bodyJson.schemaPath;
requestSchema = schemaPath;
requestTypeName = pathTypeMap.get(schemaPath);
}
else if (bodyJson.type) {
const typeName = utils$1.asArray(bodyJson.type)[0];
requestTypeName = typeName;
const schemaPath = typePathMap.get(typeName);
requestSchema = schemaPath;
}
}
}
for (const responseNode of responses) {
const statusCode = responseNode.code().value();
const responseNodeDescription = (_e = responseNode.description()) === null || _e === void 0 ? void 0 : _e.value();
for (const bodyNode of responseNode.body()) {
if (bodyNode.name().includes('application/json')) {
const bodyJson = bodyNode.toJSON();
if (bodyJson.schemaPath) {
const schemaPath = bodyJson.schemaPath;
const typeName = pathTypeMap.get(schemaPath);
if (schemaPath) {
responseByStatusCode[statusCode] = {
responseSchema: schemaPath,
responseTypeName: typeName,
};
}
}
else if (bodyJson.type) {
const typeName = utils$1.asArray(bodyJson.type)[0];
const schemaPath = typePathMap.get(typeName);
if (schemaPath) {
responseByStatusCode[statusCode] = {
responseSchema: schemaPath,
responseTypeName: typeName,
};
}
}
if (!responseByStatusCode[statusCode] && bodyJson.example) {
const responseSchema = toJsonSchema(bodyJson.example, {
required: false,
});
responseSchema.description = responseNodeDescription;
responseByStatusCode[statusCode] = {
responseSchema,
};
}
}
}
}
fieldName =
fieldName ||
getFieldNameFromPath(originalFullRelativeUrl, method, (_f = responseByStatusCode['200']) === null || _f === void 0 ? void 0 : _f.responseTypeName);
if (fieldName) {
const graphQLFieldName = utils.sanitizeNameForGraphQL(fieldName);
const operationType = ((_g = fieldTypeMap[graphQLFieldName]) !== null && _g !== void 0 ? _g : method === 'GET') ? 'query' : 'mutation';
operations.push({
type: operationType,
field: graphQLFieldName,
description,
path: fullRelativeUrl,
method,
requestSchema,
requestTypeName,
responseByStatusCode,
argTypeMap,
});
}
}
}
return {
operations,
baseUrl,
cwd,
fetch: fetch$1,
};
}
/**
* Creates a local GraphQLSchema instance from a RAML API Document.
* Everytime this function is called, the RAML file and its dependencies will be resolved on runtime.
* If you want to avoid this, use `createBundle` function to create a bundle once and save it to a storage
* then load it with `loadGraphQLSchemaFromBundle`.
*/
async function loadGraphQLSchemaFromRAML(name, options) {
const extraJSONSchemaOptions = await getJSONSchemaOptionsFromRAMLOptions(options);
return jsonSchema.loadGraphQLSchemaFromJSONSchemas(name, {
...options,
...extraJSONSchemaOptions,
});
}
/**
* Creates a bundle by downloading and resolving the internal references once
* to load the schema locally later
*/
async function createBundle(name, ramlLoaderOptions) {
const { operations, baseUrl, cwd, fetch } = await getJSONSchemaOptionsFromRAMLOptions(ramlLoaderOptions);
return jsonSchema.createBundle(name, { ...ramlLoaderOptions, baseUrl, operations, cwd, fetch });
}
Object.defineProperty(exports, 'getGraphQLSchemaFromBundle', {
enumerable: true,
get: function () {
return jsonSchema.getGraphQLSchemaFromBundle;
}
});
exports.createBundle = createBundle;
exports.default = loadGraphQLSchemaFromRAML;
exports.getJSONSchemaOptionsFromRAMLOptions = getJSONSchemaOptionsFromRAMLOptions;