@silexlabs/grapesjs-data-source
Version:
Grapesjs Data Source
890 lines • 38.7 kB
JavaScript
"use strict";
/*
* Silex website builder, free/libre no-code tool for makers.
* Copyright (c) 2023 lexoyo and Silex Labs foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GITLAB_BATCH_SIZE = exports.DEFAULT_BATCH_SIZE = exports.lightweightTypeNamesQuery = void 0;
exports.getBatchSize = getBatchSize;
exports.buildBatchTypeQuery = buildBatchTypeQuery;
const types_1 = require("../types");
const graphql_introspection_query_1 = __importDefault(require("./graphql-introspection-query"));
const dedent_js_1 = __importDefault(require("dedent-js"));
const types_2 = require("../types");
const token_1 = require("../model/token");
/**
* Lightweight query to fetch type names and kinds during datasource creation
* Also fetches queryType name to know which type is the root query type
* Does NOT fetch fields, interfaces, enums, inputs, or possibleTypes
*/
exports.lightweightTypeNamesQuery = `
query TypeNamesQuery {
__schema {
queryType {
name
}
types {
name
kind
}
}
}
`;
/**
* Simplified fragment for selective introspection query
* Fetches type name, field names, field types, and field arguments
* This keeps the query lightweight and avoids GitLab complexity limits
*/
const selectiveIntrospectionFragment = `
fragment SelectiveType on __Type {
name
kind
fields(includeDeprecated: false) {
name
args {
name
type {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
defaultValue
}
type {
kind
name
possibleTypes {
kind
name
}
ofType {
kind
name
possibleTypes {
kind
name
}
ofType {
kind
name
possibleTypes {
kind
name
}
ofType {
kind
name
possibleTypes {
kind
name
}
}
}
}
}
}
}
`;
/**
* Number of types to fetch per batch request
* Balances between number of HTTP requests and query complexity
* Default is 100, but GitLab requires smaller batches (5) due to query complexity limits
*/
exports.DEFAULT_BATCH_SIZE = 100;
exports.GITLAB_BATCH_SIZE = 5;
function getBatchSize(backendType) {
return backendType === 'gitlab' ? exports.GITLAB_BATCH_SIZE : exports.DEFAULT_BATCH_SIZE;
}
/**
* Build a selective introspection query for a batch of types
* Uses __type(name: "...") for each type
* @param typeNames - Array of type names to fetch
* @returns GraphQL query string
*/
function buildBatchTypeQuery(typeNames) {
const typeQueries = typeNames
.map(name => {
const alias = `type_${name.replace(/[^a-zA-Z0-9_]/g, '_')}`;
return ` ${alias}: __type(name: "${name}") { ...SelectiveType }`;
})
.join('\n');
return `
query BatchTypeIntrospection {
${typeQueries}
}
${selectiveIntrospectionFragment}
`;
}
/**
* GraphQL DataSource implementation
* Simple JS object that implements IDataSource interface
*/
class GraphQL {
constructor(options) {
this.type = 'graphql';
this.method = 'POST';
this.headers = {};
this.backendType = 'generic';
this.types = [];
this.queryables = [];
this.queryType = '';
this.ready = false;
this.eventListeners = {};
this.id = options.id.toString();
this.label = options.label;
this.url = options.url;
this.type = options.type;
this.method = options.method || 'POST';
this.headers = options.headers || {};
this.queryable = options.queryable;
this.readonly = options.readonly;
this.hidden = options.hidden;
this.backendType = options.backendType || 'generic';
this.disabledTypes = options.disabledTypes;
}
// Simple event handling
on(event, callback) {
if (!this.eventListeners[event]) {
this.eventListeners[event] = [];
}
this.eventListeners[event].push(callback);
}
off(event, callback) {
if (!this.eventListeners[event])
return;
if (callback) {
this.eventListeners[event] = this.eventListeners[event].filter(cb => cb !== callback);
}
else {
this.eventListeners[event] = [];
}
}
trigger(event, ...args) {
if (!this.eventListeners[event])
return;
this.eventListeners[event].forEach(callback => callback(...args));
}
/**
* Fetch only type names and kinds from the GraphQL endpoint
* This is a lightweight query used during datasource creation
* Returns an object with types array and queryTypeName
*/
fetchTypeNames() {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c;
try {
const result = yield this.call(exports.lightweightTypeNamesQuery);
if (!((_b = (_a = result.data) === null || _a === void 0 ? void 0 : _a.__schema) === null || _b === void 0 ? void 0 : _b.types)) {
throw new Error(`Invalid response: ${JSON.stringify(result)}`);
}
const queryTypeName = ((_c = result.data.__schema.queryType) === null || _c === void 0 ? void 0 : _c.name) || 'Query';
const types = result.data.__schema.types
// Filter out introspection types (starting with __)
.filter((type) => !type.name.startsWith('__'));
return { types, queryTypeName };
}
catch (e) {
console.error('[GraphQL] Failed to fetch type names:', e.message);
throw new Error(`Failed to fetch type names: ${e.message}`);
}
});
}
/**
* Get default enabled types based on backend type
* @param backendType - The backend type (gitlab, wordpress, generic)
* @param allTypes - Array of all available types from the schema with their kinds
* @param queryTypeName - The name of the query type from the schema (e.g., 'Query', 'RootQuery')
* @returns Array of type names that should be enabled by default
*/
static getDefaultEnabledTypes(backendType, allTypes, queryTypeName = 'Query') {
// Get all type names for filtering
const allTypeNames = allTypes.map(t => t.name);
// Automatically detect all SCALAR types from the schema
// This includes String, Boolean, Int, Float, ID, and any custom scalars like DateTime, JSON, etc.
const scalarTypeNames = allTypes
.filter(t => t.kind === 'SCALAR')
.map(t => t.name);
switch (backendType) {
case 'gitlab': {
// GitLab: Essential types + connections for listing
const gitlabCoreTypes = [
'Project',
'Namespace',
'User',
'Group',
'Repository',
'Commit',
'Branch',
'MergeRequest',
'Issue',
'Pipeline',
'Job',
'Release',
'Milestone',
'Label',
'Note',
'Discussion',
'Snippet',
'Board',
'Epic',
'Vulnerability',
'Package',
'ContainerRepository',
'Environment',
'Deployment',
'CiRunner',
'Topic',
'PageInfo',
];
const gitlabDefaults = [
...scalarTypeNames,
queryTypeName,
...gitlabCoreTypes,
// Include connection types for pagination (e.g., GroupConnection, ProjectConnection)
...allTypeNames.filter(name => {
// Exclude input/enum types
if (name.endsWith('Input') || name.endsWith('Enum'))
return false;
// Include connections and edges for core types
return (name.endsWith('Connection') ||
name.endsWith('Edge') ||
// Include core type variations
gitlabCoreTypes.some(core => name.startsWith(core)));
}),
];
return allTypeNames.filter(name => gitlabDefaults.includes(name));
}
case 'wordpress': {
// WordPress/WPGraphQL: Blacklist approach for headless CMS
// All types are enabled by default, except those explicitly blacklisted
// This ensures CPTs (like ACF custom post types) are included automatically
// Get all INPUT_OBJECT types (used for mutations, not queries)
const inputTypes = allTypes
.filter(t => t.kind === 'INPUT_OBJECT')
.map(t => t.name);
// Types to blacklist (not useful for static site generation)
const blacklistedTypes = [
// Mutation root
'RootMutation',
// Settings (admin only)
'Settings',
'DiscussionSettings',
'GeneralSettings',
'ReadingSettings',
'WritingSettings',
// Admin-only types
'Plugin',
'Theme',
'UserRole',
'Avatar',
// Content templates (WP internal, not CPTs)
'ContentTemplate',
'DefaultTemplate',
];
// Patterns to blacklist
const blacklistPatterns = [
// All mutation inputs and payloads
/^Create.+Input$/,
/^Update.+Input$/,
/^Delete.+Input$/,
/^Register.+Input$/,
/^Reset.+Input$/,
/^Restore.+Input$/,
/^Send.+Input$/,
/Payload$/,
// Admin connections
/^RootQueryToPlugin/,
/^RootQueryToTheme/,
/^RootQueryToUserRole/,
// Plugin/Theme/UserRole related
/Plugin(?:Connection|Edge|PageInfo|Status)/,
/Theme(?:Connection|Edge|PageInfo)/,
/UserRole(?:Connection|Edge|PageInfo)/,
// Enqueued assets (internal WP)
/Enqueued/,
/Script(?!$)/, // Exclude Script-related but not if it's just "Script"
/Stylesheet/,
// Revisions (internal WP)
/Revision/,
// Comments (usually not needed for static sites)
/Comment/,
/Commenter/,
// Post formats (rarely used)
/PostFormat/,
// Generic content types (not needed if querying specific collections)
/^RootQueryToContentNode/,
];
const isBlacklisted = (name) => {
// Blacklist all INPUT_OBJECT types (mutation inputs)
if (inputTypes.includes(name))
return true;
// Blacklist explicitly named types
if (blacklistedTypes.includes(name))
return true;
// Blacklist by pattern
return blacklistPatterns.some(pattern => pattern.test(name));
};
// Return all types except blacklisted ones
return allTypeNames.filter(name => !isBlacklisted(name));
}
case 'strapi': {
// Strapi v4/v5 GraphQL: Blacklist approach for headless CMS
// Strapi generates types for content types, media, and internal admin types
// Get all INPUT_OBJECT types (used for mutations, not queries)
const inputTypes = allTypes
.filter(t => t.kind === 'INPUT_OBJECT')
.map(t => t.name);
// Types to blacklist (not useful for static site generation)
const blacklistedTypes = [
// Mutation root
'Mutation',
// Generic internal type
'GenericMorph',
];
// Patterns to blacklist
const blacklistPatterns = [
// Mutation payloads
/Payload$/,
// Users & Permissions plugin (admin)
/^UsersPermissions/,
// Upload folder management (admin)
/^UploadFolder/,
// i18n internal types
/^I18N/,
// Entity wrappers (Strapi v4 internal)
/EntityResponse$/,
/EntityResponseCollection$/,
// Content type metadata (admin)
/^ContentType/,
// Admin panel types
/^Admin/,
];
const isBlacklisted = (name) => {
// Blacklist all INPUT_OBJECT types (mutation inputs)
if (inputTypes.includes(name))
return true;
// Blacklist explicitly named types
if (blacklistedTypes.includes(name))
return true;
// Blacklist by pattern
return blacklistPatterns.some(pattern => pattern.test(name));
};
// Return all types except blacklisted ones
return allTypeNames.filter(name => !isBlacklisted(name));
}
case 'supabase': {
// Supabase (pg_graphql): Blacklist approach
// pg_graphql generates types from PostgreSQL tables
// Get all INPUT_OBJECT types (used for mutations, not queries)
const inputTypes = allTypes
.filter(t => t.kind === 'INPUT_OBJECT')
.map(t => t.name);
// Types to blacklist (not useful for static site generation)
const blacklistedTypes = [
// Mutation root
'Mutation',
// Cursor type (internal pagination)
'Cursor',
];
// Patterns to blacklist
const blacklistPatterns = [
// Mutation-related types
/InsertInput$/,
/UpdateInput$/,
/DeleteInput$/,
/InsertResponse$/,
/UpdateResponse$/,
/DeleteResponse$/,
// Ordering types (query params, not data)
/OrderBy$/,
/OrderByDirection$/,
// Filter types (query params, not data)
/Filter$/,
/FilterInput$/,
// Edge types (keep Connection but not Edge for simpler queries)
/Edge$/,
];
const isBlacklisted = (name) => {
// Blacklist all INPUT_OBJECT types (mutation inputs)
if (inputTypes.includes(name))
return true;
// Blacklist explicitly named types
if (blacklistedTypes.includes(name))
return true;
// Blacklist by pattern
return blacklistPatterns.some(pattern => pattern.test(name));
};
// Return all types except blacklisted ones
return allTypeNames.filter(name => !isBlacklisted(name));
}
case 'generic':
default:
// Generic: All types checked by default
return [...allTypeNames];
}
}
/**
* @throws Error
*/
triggerError(message) {
console.error('GraphQL error:', message);
this.trigger(types_1.DATA_SOURCE_ERROR, message, this);
throw new Error(message);
}
loadData() {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d, _e, _f;
try {
let schemaTypes;
let queryTypeName;
// If disabledTypes is set, compute enabled types and use selective introspection
if (this.disabledTypes && this.disabledTypes.length > 0) {
// Fetch all type names (lightweight query)
const lightweightResult = yield this.call(exports.lightweightTypeNamesQuery);
if (!((_b = (_a = lightweightResult.data) === null || _a === void 0 ? void 0 : _a.__schema) === null || _b === void 0 ? void 0 : _b.types)) {
return this.triggerError(`Invalid lightweight response: ${JSON.stringify(lightweightResult)}`);
}
const allTypeNames = lightweightResult.data.__schema.types
.map(t => t.name)
.filter(name => !name.startsWith('__'));
// Get the queryType name from the lightweight query
const lightweightQueryTypeName = ((_c = lightweightResult.data.__schema.queryType) === null || _c === void 0 ? void 0 : _c.name) || 'Query';
// Compute enabled types by excluding disabled ones (blacklist logic)
// Always include queryType and SCALAR types - they cannot be blacklisted
const scalarTypeNames = lightweightResult.data.__schema.types
.filter(t => t.kind === 'SCALAR' && !t.name.startsWith('__'))
.map(t => t.name);
const enabledTypeNames = allTypeNames.filter(name => !this.disabledTypes.includes(name));
// Ensure queryType is always included (e.g., Query, RootQuery, etc.)
if (!enabledTypeNames.includes(lightweightQueryTypeName) && allTypeNames.includes(lightweightQueryTypeName)) {
enabledTypeNames.push(lightweightQueryTypeName);
}
// Ensure all SCALAR types are always included
for (const scalarName of scalarTypeNames) {
if (!enabledTypeNames.includes(scalarName)) {
enabledTypeNames.push(scalarName);
}
}
// Split types into batches (smaller batches for GitLab due to query complexity limits)
const batchSize = getBatchSize(this.backendType);
const batches = [];
for (let i = 0; i < enabledTypeNames.length; i += batchSize) {
batches.push(enabledTypeNames.slice(i, i + batchSize));
}
// Fetch batches in parallel
const batchResults = yield Promise.all(batches.map((batch) => __awaiter(this, void 0, void 0, function* () {
try {
const query = buildBatchTypeQuery(batch);
const result = yield this.call(query);
// Extract types from aliased responses
const types = [];
for (const [key, value] of Object.entries(result.data || {})) {
if (key.startsWith('type_') && value !== null) {
types.push(value);
}
}
return types;
}
catch (e) {
console.warn('[GraphQL] Failed to fetch batch:', e.message);
return [];
}
})));
// Flatten batch results
schemaTypes = batchResults.flat();
queryTypeName = lightweightQueryTypeName;
}
else {
// No blacklist - use full introspection query
const result = yield this.call(graphql_introspection_query_1.default);
if (!((_e = (_d = result.data) === null || _d === void 0 ? void 0 : _d.__schema) === null || _e === void 0 ? void 0 : _e.types)) {
return this.triggerError(`Invalid response: ${JSON.stringify(result)}`);
}
schemaTypes = result.data.__schema.types;
queryTypeName = (_f = result.data.__schema.queryType) === null || _f === void 0 ? void 0 : _f.name;
}
if (!queryTypeName) {
return this.triggerError('Invalid response, queryType not found');
}
const allTypes = schemaTypes.map((type) => type.name)
.concat(types_1.builtinTypeIds);
const query = schemaTypes.find((type) => type.name === queryTypeName);
if (!query) {
return this.triggerError(`Query type "${queryTypeName}" not found in schema. Make sure to enable it.`);
}
// Get non-queryable types
const nonQueryables = schemaTypes
// Filter out introspection types
.filter((type) => !type.name.startsWith('__'))
// Filter out types that are in Query (the queryables are handled separately)
.filter((type) => { var _a; return !((_a = query === null || query === void 0 ? void 0 : query.fields) === null || _a === void 0 ? void 0 : _a.find((field) => field.name === type.name)); })
// Map to Type
.map((type) => this.graphQLToType(allTypes, type, 'SCALAR', false))
// Add builtin types
.concat(types_1.builtinTypes);
// Get queryable types
const queryableTypes = (query.fields || [])
// Map to GQLType, keeping kind for later
.map((field) => ({
type: Object.assign(Object.assign({}, schemaTypes.find((type) => type.name === this.getOfTypeProp('name', field.type, field.name))), { name: field.name }),
kind: this.ofKindToKind(field.type),
}))
// Filter out types that were excluded
.filter(({ type }) => type.fields !== undefined)
// Map to Type
.map(({ type, kind }) => this.graphQLToType(allTypes, type, kind, true));
// Get all queryables as fields
const queryableFields = (query.fields || [])
// Filter out fields whose types were not fetched
.filter((field) => {
const typeName = this.getOfTypeProp('name', field.type, field.name);
// Include if the type exists in schemaTypes or is a builtin type
return schemaTypes.some(t => t.name === typeName) || types_1.builtinTypeIds.includes(typeName);
})
// Map to Field
.map((field) => this.graphQLToField(field));
// Return all types, queryables and non-queryables
return [queryableTypes.concat(nonQueryables), queryableFields, queryTypeName];
}
catch (e) {
return this.triggerError(`GraphQL introspection failed: ${e.message}`);
}
});
}
graphQLToField(field) {
var _a;
const kind = this.ofKindToKind(field.type);
return {
id: field.name,
dataSourceId: this.id,
label: field.name,
typeIds: this.graphQLToTypes(field),
kind: kind ? this.graphQLToKind(kind) : 'unknown',
arguments: (_a = field.args) === null || _a === void 0 ? void 0 : _a.map(arg => ({
name: arg.name,
typeId: this.getOfTypeProp('name', arg.type, arg.name),
defaultValue: arg.defaultValue,
})),
};
}
/**
* Recursively search for a property on a GraphQL type
* Check the deepest values in ofType first
*/
getOfTypeProp(prop, type, defaultValue) {
const result = this.getOfTypePropRecursive(prop, type);
if (result)
return result;
if (defaultValue)
return defaultValue;
throw new Error(`Type ${JSON.stringify(type)} has no property ${prop} and no default was provided`);
}
getOfTypePropRecursive(prop, type) {
if (!type) {
console.error('Invalid type', type);
throw new Error('Invalid type');
}
if (type.ofType) {
const ofTypeResult = this.getOfTypePropRecursive(prop, type.ofType);
if (ofTypeResult)
return ofTypeResult;
}
return type[prop];
}
/**
* Recursively search for a property on a GraphQL type
* Handles Union types with possibleTypes
* Handles list and object and non-null types with ofType
*/
graphQLToTypes(field) {
const possibleTypes = this.getOfTypeProp('possibleTypes', field.type, []);
if (possibleTypes.length > 0) {
return possibleTypes.map(type => type.name);
}
const typeName = this.getOfTypeProp('name', field.type, field.name);
return [typeName];
}
/**
* Convert GraphQL kind to FieldKind
* @throws Error if kind is not valid or is NON_NULL
*/
graphQLToKind(kind) {
switch (kind) {
case 'LIST': return 'list';
case 'OBJECT': return 'object';
case 'SCALAR': return 'scalar';
case 'UNION':
case 'NON_NULL':
default:
throw new Error(`Unable to find a valid kind for ${kind}`);
}
}
/**
* Check if a GraphQL kind has a valid FieldKind equivalent
*/
validKind(kind) {
return ['LIST', 'OBJECT', 'SCALAR'].includes(kind);
}
/**
* Recursively search for a GraphQL kind of type list, object or scalar
*/
ofKindToKind(ofKind) {
if (ofKind.possibleTypes) {
const foundKind = ofKind.possibleTypes
.reduce((prev, type) => {
if (!prev)
return type.kind;
if (prev !== type.kind) {
throw new Error(`Unable to find a valid kind for ${ofKind.kind}. Union types with different kind is not supported`);
}
return prev;
}, null);
if (!foundKind) {
console.error('Unable to find a valid kind (1)', ofKind);
return null;
}
return foundKind;
}
if (this.validKind(ofKind.kind))
return ofKind.kind;
if (ofKind.ofType)
return this.ofKindToKind(ofKind.ofType);
// This happens when the type is missing
// Remove the warning because it happens with directus and polutes the logs
// console.error('Unable to find a valid kind (2)', ofKind)
return null;
}
/**
* Convert a GraphQL type to a Type
*/
graphQLToType(allTypes, type, kind, queryable) {
var _a, _b, _c;
const queryableOverride = this.queryable;
const result = {
id: type.name,
dataSourceId: this.id,
label: type.name,
fields: (_c = (_b = (_a = type.fields) === null || _a === void 0 ? void 0 : _a.filter((field) => allTypes.includes(this.getOfTypeProp('name', field.type, field.name)))) === null || _b === void 0 ? void 0 : _b.map(field => this.graphQLToField(field))) !== null && _c !== void 0 ? _c : [],
queryable: queryable && (!queryableOverride || queryableOverride.includes(type.name)),
};
return result;
}
/**
* Connect to the GraphQL endpoint and load the schema
* This has to be implemented as it is a DataSource method
*/
connect() {
return __awaiter(this, void 0, void 0, function* () {
try {
// const result = await this.call(`
// query {
// __typename
// }
// `) as any
// if (!result?.data?.__typename) return this.triggerError(`Invalid response: ${JSON.stringify(result)}`)
const [types, fields, queryType] = yield this.loadData();
if (types.length === 0)
return this.triggerError('No types found in GraphQL schema');
if (fields.length === 0)
return this.triggerError('No fields found in GraphQL schema');
if (!queryType)
return this.triggerError('No query type found in GraphQL schema');
this.types = types;
this.queryables = fields;
this.queryType = queryType;
if (this.ready) {
this.trigger(types_1.DATA_SOURCE_CHANGED, this);
}
else {
this.ready = true;
this.trigger(types_1.DATA_SOURCE_READY, this);
}
}
catch (e) {
return this.triggerError(`GraphQL connection failed: ${e.message}`);
}
});
}
/**
* Check if the DataSource is ready
* This has to be implemented as it is a DataSource method
*/
isConnected() {
return this.ready;
}
/**
* Get all types
* This has to be implemented as it is a DataSource method
*/
getTypes() {
if (!this.ready) {
console.error('DataSource is not ready. Attempted to get types before ready status was achieved.');
throw new Error('DataSource is not ready. Ensure it is connected and ready before querying.');
}
if (this.types.length === 0) {
console.error('No types available. It seems the data source may not be connected or the schema is incomplete.', this.ready);
throw new Error('No types found. The data source may not be connected or there might be an issue with the schema.');
}
return this.types;
}
/**
* Get all queryable fields
* This has to be implemented as it is a DataSource method
*/
getQueryables() {
return this.queryables;
}
/**
* Call the GraphQL endpoint
*/
call(query) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
// Retrieve the URL for the GraphQL endpoint
const url = this.url;
if (!url)
return this.triggerError('Missing GraphQL URL'); // Ensure the URL is provided
// Retrieve the headers for the GraphQL request
const headers = this.headers;
if (!headers)
return this.triggerError('Missing GraphQL headers'); // Ensure headers are provided
// Ensure the Content-Type header is set to 'application/json', normalizing the case
const key = Object.keys(headers).find(name => name.toLowerCase() === 'content-type');
headers[key || 'Content-Type'] = headers[key || 'Content-Type'] || 'application/json';
// Retrieve the HTTP method (defaulting to 'POST' for GraphQL queries)
const method = (_a = this.method) !== null && _a !== void 0 ? _a : 'POST';
// Make the HTTP request to the GraphQL endpoint
const response = yield fetch(url, Object.assign({ method,
headers }, (method === 'POST' ? {
body: JSON.stringify({ query }),
} : {})));
// Handle non-OK responses with detailed error logging
if (!(response === null || response === void 0 ? void 0 : response.ok)) {
console.error('GraphQL call failed', response === null || response === void 0 ? void 0 : response.status, response === null || response === void 0 ? void 0 : response.statusText, query);
return this.triggerError(`GraphQL call failed with \`${response === null || response === void 0 ? void 0 : response.statusText}\` and status ${response === null || response === void 0 ? void 0 : response.status}`);
}
// Return the parsed JSON response
return response.json();
});
}
/**
* Build a GraphQL query from a tree
*/
getQuery(children) {
return this.getQueryRecursive({
// Add the main query object which is the root of the tree
token: {
dataSourceId: this.id,
fieldId: 'query',
kind: 'object',
typeIds: [this.queryType],
},
children,
});
}
getQueryRecursive(tree, indent = '', fragment = '') {
// Check if the tree is a fragment
const typeOrFragment = fragment ? `...on ${fragment}` : `${tree.token.fieldId}${(0, token_1.buildArgs)(tree.token.options)}`;
// Build the value
switch (tree.token.kind) {
case 'scalar':
if (tree.token.fieldId === types_2.FIXED_TOKEN_ID)
return '';
return indent + typeOrFragment;
case 'object':
case 'list': {
const types = this.getTypes().filter(t => { var _a; return (_a = tree.token.typeIds) === null || _a === void 0 ? void 0 : _a.includes(t.id); });
if (types.length === 0) {
throw new Error(`Type not found for ${tree.token.fieldId} (${tree.token.typeIds})`);
}
else if (types.length > 1)
throw new Error(`Multiple types found for ${tree.token.fieldId}`);
const type = types[0];
const fieldTypes = tree.children
.map(child => {
const fieldType = type.fields.find(f => f.id === child.token.fieldId);
if (!fieldType) {
// Not a queryable type
return null;
}
return {
fieldType,
child,
};
})
// Remove non-queryable types
.filter(fieldType => fieldType !== null);
// Handle fragments
const fragments = fieldTypes
.filter(({ fieldType }) => fieldType.typeIds.length > 1)
.map(({ child }) => {
return {
query: this.getQueryRecursive(child, indent + ' ', child.token.typeIds[0]),
child,
};
});
const fragmentsQuery = fragments
.map(({ query, child }) => (0, dedent_js_1.default) `
${indent}${child.token.fieldId} {
${query}
}
`)
.join('\n');
// Handle simple case, no fragment
const childQuery = fieldTypes
.filter(({ fieldType }) => fieldType.typeIds.length === 1)
.map(({ child }) => {
return this.getQueryRecursive(child, indent + ' ');
})
.join('\n');
return (0, dedent_js_1.default) `${indent}${typeOrFragment} {
${indent} __typename
${childQuery}
${fragmentsQuery}
${indent}}`;
}
default:
console.error('Unable to build GraphQL query', tree);
throw new Error(`Unable to build GraphQL query: unable to build tree ${JSON.stringify(tree)}`);
}
}
fetchValues(query) {
return __awaiter(this, void 0, void 0, function* () {
const result = yield this.call(query);
return result.data;
});
}
}
exports.default = GraphQL;
//# sourceMappingURL=GraphQL.js.map