adminjs-graphql
Version:
adminjs GraphQL adapter
256 lines • 12.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.GraphQLConnection = void 0;
const graphql_1 = require("graphql");
const GraphQLClient_1 = require("./GraphQLClient");
const GraphQLProperty_1 = require("./GraphQLProperty");
/**
* GraphQLConnection connects to a GraphQL API, and initializes a list of
* configured resources with data from the remote API schema, so that they
* can be used as AdminBro resources.
*/
class GraphQLConnection {
constructor(resources, options, onError) {
var _a, _b;
this.resources = resources;
this.onError = onError;
this.tag = "GraphQLConnection";
this.name = (_a = options === null || options === void 0 ? void 0 : options.name) !== null && _a !== void 0 ? _a : "graphql";
const url = (_b = options === null || options === void 0 ? void 0 : options.url) !== null && _b !== void 0 ? _b : "http://localhost:3000/graphql";
this.client = new GraphQLClient_1.GraphQLClient(url, options === null || options === void 0 ? void 0 : options.agentOptions);
this.headers = options === null || options === void 0 ? void 0 : options.headers;
}
get r() {
return this.resources.reduce((map, resource) => {
map[resource.id] = resource;
return map;
}, {});
}
async init() {
const fullSchema = await this.fetchSchema();
await this.inflateResources(fullSchema);
}
async fetchSchema() {
const query = (0, graphql_1.getIntrospectionQuery)({ descriptions: false });
const result = await this.request(query);
return (0, graphql_1.buildClientSchema)(result);
}
static graphQLTypeToPropertyType(graphQLType) {
switch (graphQLType.name) {
case "String":
case "ID":
return "string";
case "Float":
return "float";
case "Int":
return "number";
case "Bool":
return "boolean";
case "Date":
return "datetime";
default:
return "mixed";
}
}
async inflateResources(schema) {
this.resources
.map((res) => res)
.map((resource) => {
var _a;
const findMapping = resource.findOne(42);
let parsed = typeof findMapping.query === "string"
? (0, graphql_1.parse)(findMapping.query)
: findMapping.query;
const typeInfo = new graphql_1.TypeInfo(schema);
const path = [];
resource.typeMap = new Map();
resource.connection = this;
resource.tag = "GraphQLResource";
parsed = expandFragments(parsed);
const operationDefinition = parsed.definitions.find((def) => def.kind === "OperationDefinition");
if (!operationDefinition) {
throw new Error("Document without operation is not allowed");
}
const toplevelSelections = operationDefinition.selectionSet.selections;
if (toplevelSelections.length !== 1) {
throw new Error("Top level selections must contain exactly one field");
}
const topNode = operationDefinition;
// Initialize with "root" object
const objectStack = [[]];
const propertyMap = new Map();
(0, graphql_1.visit)(topNode, (0, graphql_1.visitWithTypeInfo)(typeInfo, {
Field: {
enter: (field) => {
var _a, _b, _c, _d, _e;
const parentType = typeInfo.getParentType();
if ((parentType === null || parentType === void 0 ? void 0 : parentType.name) === "Query" ||
(parentType === null || parentType === void 0 ? void 0 : parentType.name) === "Mutation") {
return;
}
const fieldName = field.name.value;
let graphQLType = typeInfo.getType();
if (!graphQLType) {
throw new Error(`Unexpected empty type for field "${fieldName}" of resource "${resource.id}"`);
}
let isArray = false;
let isRequired = false;
while ((0, graphql_1.isWrappingType)(graphQLType)) {
if (graphQLType instanceof graphql_1.GraphQLList) {
isArray = true;
}
else if (graphQLType instanceof graphql_1.GraphQLNonNull &&
!isArray) {
isRequired = true;
}
graphQLType =
graphQLType.ofType;
}
const namedType = graphQLType;
let enumValues;
if (namedType instanceof graphql_1.GraphQLEnumType) {
enumValues = namedType
.getValues()
.map((val) => val.value);
}
const parentPath = path.join(".");
const propertyPath = [...path, fieldName].join(".");
let propertyType;
let referencing;
if (namedType instanceof graphql_1.GraphQLObjectType) {
const objectFields = namedType.getFields();
const selections = (_b = (_a = field.selectionSet) === null || _a === void 0 ? void 0 : _a.selections) !== null && _b !== void 0 ? _b : [];
if (selections.length === 1 &&
selections[0].kind === "Field") {
const fieldName = selections[0].name.value;
const objectField = objectFields[fieldName];
if (!objectField) {
throw new Error(`Field ${fieldName} is not in ${namedType.name}`);
}
const fieldType = objectField.type;
if (typeIsID(fieldType)) {
propertyType = "reference";
referencing = namedType.name;
}
}
}
if (!propertyType) {
propertyType =
(propertyPath in
((_c = resource.referenceFields) !== null && _c !== void 0 ? _c : {}) &&
"reference") ||
((enumValues === null || enumValues === void 0 ? void 0 : enumValues.length) && "string") ||
GraphQLConnection.graphQLTypeToPropertyType(namedType);
}
const isSortable = resource.sortableFields
? resource.sortableFields.includes(propertyPath)
: propertyType != "reference";
const parentProperty = propertyMap.get(parentPath);
const useFullPath = !resource.makeSubproperties &&
!((parentProperty === null || parentProperty === void 0 ? void 0 : parentProperty.type()) === "mixed" &&
(parentProperty === null || parentProperty === void 0 ? void 0 : parentProperty.isArray()));
const property = new GraphQLProperty_1.GraphQLPropertyAdapter({
path: useFullPath
? propertyPath
: fieldName,
type: propertyType,
isId: namedType.name === "ID" &&
propertyType !== "reference",
isSortable: isSortable,
referencing: referencing !== null && referencing !== void 0 ? referencing : (_d = resource.referenceFields) === null || _d === void 0 ? void 0 : _d[propertyPath],
enumValues,
isArray,
isRequired,
});
objectStack[objectStack.length - 1].push(property);
propertyMap.set(propertyPath, property);
(_e = resource.typeMap) === null || _e === void 0 ? void 0 : _e.set(propertyPath, namedType);
if (field.selectionSet) {
path.push(fieldName);
objectStack.push([]);
}
},
leave: (field) => {
var _a;
const parentType = (_a = typeInfo.getParentType()) === null || _a === void 0 ? void 0 : _a.name;
if (parentType === "Query" ||
parentType === "Mutation") {
return;
}
if (field.selectionSet) {
path.pop();
const currentObject = objectStack.pop();
if (currentObject === undefined) {
throw new Error("Unexpected empty object");
}
const lastObject = objectStack[objectStack.length - 1];
const lastProperty = lastObject[lastObject.length - 1];
if (lastProperty &&
((lastProperty.type() === "mixed" &&
lastProperty.isArray()) ||
resource.makeSubproperties)) {
lastProperty.setSubProperties(currentObject);
}
else if (currentObject.length !== 1 ||
!currentObject[0].isId()) {
lastObject.push(...currentObject);
}
}
},
},
}));
resource.properties = (_a = objectStack
.pop()) === null || _a === void 0 ? void 0 : _a.filter((prop) => prop.type() !== "mixed" ||
prop.subProperties().length);
});
}
formatGraphQLErrors(errors) {
return ("GraphQL request error: " +
errors.map((error) => error.message).join(", "));
}
async request(document, variables) {
var _a, _b, _c, _d;
try {
const headers = (_a = this.headers) === null || _a === void 0 ? void 0 : _a.call(this);
const response = await this.client.request(document, variables, headers);
if ((_b = response.errors) === null || _b === void 0 ? void 0 : _b.length) {
this.reportAndThrow(new Error(this.formatGraphQLErrors(response.errors)), response.errors);
}
return response.data;
}
catch (thrown) {
let error = thrown;
const axiosError = error;
const graphQLErrors = (_d = (_c = axiosError.response) === null || _c === void 0 ? void 0 : _c.data) === null || _d === void 0 ? void 0 : _d.errors;
if (graphQLErrors) {
error = new Error(this.formatGraphQLErrors(graphQLErrors));
}
this.reportAndThrow(error, graphQLErrors);
}
}
reportAndThrow(error, originalErrors) {
var _a;
(_a = this.onError) === null || _a === void 0 ? void 0 : _a.call(this, error, originalErrors);
throw error;
}
}
exports.GraphQLConnection = GraphQLConnection;
function expandFragments(node) {
const fragmentDefinitions = node.definitions.filter((def) => def.kind === "FragmentDefinition");
return (0, graphql_1.visit)(node, {
FragmentSpread: (spread) => {
const fragment = fragmentDefinitions.find((def) => def.name.value === spread.name.value);
if (!fragment) {
throw new Error("Invalid spread reference");
}
return fragment.selectionSet;
},
});
}
function typeIsID(fieldType) {
while ((0, graphql_1.isWrappingType)(fieldType)) {
fieldType = fieldType.ofType;
}
return fieldType === graphql_1.GraphQLID;
}
//# sourceMappingURL=GraphQLConnection.js.map