@settlemint/sdk-thegraph
Version:
TheGraph integration module for SettleMint SDK, enabling querying and indexing of blockchain data through subgraphs
436 lines (432 loc) • 15.3 kB
JavaScript
/* SettleMint The Graph SDK - Indexing Protocol */
//#region rolldown:runtime
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
const __settlemint_sdk_utils_http = __toESM(require("@settlemint/sdk-utils/http"));
const __settlemint_sdk_utils_runtime = __toESM(require("@settlemint/sdk-utils/runtime"));
const __settlemint_sdk_utils_validation = __toESM(require("@settlemint/sdk-utils/validation"));
const gql_tada = __toESM(require("gql.tada"));
const graphql_request = __toESM(require("graphql-request"));
const zod = __toESM(require("zod"));
const es_toolkit = __toESM(require("es-toolkit"));
const es_toolkit_compat = __toESM(require("es-toolkit/compat"));
const graphql = __toESM(require("graphql"));
//#region src/utils/pagination.ts
const THE_GRAPH_LIMIT = 500;
const FIRST_ARG = "first";
const SKIP_ARG = "skip";
const FETCH_ALL_DIRECTIVE = "fetchAll";
/**
* Detects and strips @fetchAll directives from a GraphQL document
*
* @param {DocumentNode} document - The GraphQL document to process
* @returns {Object} Processed document and list of fields with @fetchAll
*
* @remarks
* This function:
* - Identifies fields decorated with @fetchAll directive
* - Removes the directive from the AST (The Graph doesn't recognize it)
* - Returns both the cleaned document and a list of fields to auto-paginate
*/
function stripFetchAllDirective(document) {
const fetchAllFields = new Set();
const documentNode = typeof document === "string" ? (0, graphql.parse)(document) : document;
const strippedDocument = (0, graphql.visit)(documentNode, { Field(node) {
if (node.directives && node.directives.length > 0) {
const hasFetchAll = node.directives.some((dir) => dir.name.value === FETCH_ALL_DIRECTIVE);
if (hasFetchAll) {
const fieldIdentifier = node.alias?.value || node.name.value;
fetchAllFields.add(fieldIdentifier);
return {
...node,
directives: node.directives.filter((dir) => dir.name.value !== FETCH_ALL_DIRECTIVE)
};
}
}
return node;
} });
return {
document: strippedDocument,
fetchAllFields
};
}
/**
* Custom merge function for deep object merging with special handling for lists
*
* @param {unknown} target - The target object or value to merge into
* @param {unknown} source - The source object or value to merge from
* @returns {unknown} Merged result with preservation of arrays and specific merge logic
*
* @remarks
* Key behaviors:
* - Preserves existing arrays without merging
* - Handles null and undefined values
* - Performs deep merge for nested objects
* - Prioritizes source values for primitives
*
*/
function customMerge(target, source) {
if (source == null) return target;
if (target == null) return source;
if ((0, es_toolkit_compat.isArray)(source)) {
return source;
}
if (typeof target !== "object" || typeof source !== "object") {
return source;
}
const targetObj = target;
const sourceObj = source;
const result = { ...targetObj };
for (const key in sourceObj) {
if (Object.hasOwn(sourceObj, key)) {
result[key] = key in result ? customMerge(result[key], sourceObj[key]) : sourceObj[key];
}
}
return result;
}
function extractFetchAllFields(document, variables, fetchAllFields) {
const fields = [];
const pathStack = [];
(0, graphql.visit)(document, { Field: {
enter: (node) => {
const fieldIdentifier = node.alias?.value || node.name.value;
pathStack.push(fieldIdentifier);
if (node.name.value.startsWith("__")) {
return;
}
let firstValue;
let skipValue;
const otherArgs = [];
if (node.arguments) {
for (const arg of node.arguments) {
if (arg.name.value === FIRST_ARG) {
if (arg.value.kind === graphql.Kind.INT) {
firstValue = Number.parseInt(arg.value.value);
} else if (arg.value.kind === graphql.Kind.VARIABLE && variables) {
const varName = arg.value.name.value;
const varValue = variables[varName];
firstValue = typeof varValue === "number" ? varValue : undefined;
}
} else if (arg.name.value === SKIP_ARG) {
if (arg.value.kind === graphql.Kind.INT) {
skipValue = Number.parseInt(arg.value.value);
} else if (arg.value.kind === graphql.Kind.VARIABLE && variables) {
const varName = arg.value.name.value;
const varValue = variables[varName];
skipValue = typeof varValue === "number" ? varValue : undefined;
}
} else {
otherArgs.push(arg);
}
}
}
const fieldIdentifierForDirective = node.alias?.value || node.name.value;
const hasFetchAllDirective = fetchAllFields?.has(fieldIdentifierForDirective);
if (hasFetchAllDirective) {
const parentFetchAllField = fields.find((field) => pathStack.join(",").startsWith(field.path.join(",")));
if (parentFetchAllField) {
throw new Error(`Nesting of @fetchAll directive is not supported: ${pathStack.join(".")} is a child of ${parentFetchAllField.path.join(".")}`);
}
fields.push({
path: [...pathStack],
fieldName: node.name.value,
firstValue: firstValue ?? THE_GRAPH_LIMIT,
skipValue: skipValue ?? 0,
otherArgs
});
}
},
leave: () => {
pathStack.pop();
}
} });
return fields;
}
function createSingleFieldQuery(document, targetField, skip, first) {
const targetPath = [...targetField.path];
const pathStack = [];
return (0, graphql.visit)(document, { Field: {
enter: (node) => {
const fieldIdentifier = node.alias?.value || node.name.value;
pathStack.push(fieldIdentifier);
const onPath = pathStack.every((segment, i) => i >= targetPath.length || segment === targetPath[i]);
if (!onPath) {
pathStack.pop();
return null;
}
const isTarget = pathStack.length === targetPath.length && pathStack.every((segment, i) => segment === targetPath[i]);
if (isTarget) {
const newArgs = [...targetField.otherArgs];
newArgs.push({
kind: graphql.Kind.ARGUMENT,
name: {
kind: graphql.Kind.NAME,
value: FIRST_ARG
},
value: {
kind: graphql.Kind.INT,
value: first.toString()
}
}, {
kind: graphql.Kind.ARGUMENT,
name: {
kind: graphql.Kind.NAME,
value: SKIP_ARG
},
value: {
kind: graphql.Kind.INT,
value: skip.toString()
}
});
return {
...node,
arguments: newArgs
};
}
return undefined;
},
leave: () => {
pathStack.pop();
}
} });
}
function createNonListQuery(document, listFields) {
let hasFields = false;
const pathStack = [];
const filtered = (0, graphql.visit)(document, { Field: {
enter: (node) => {
const fieldIdentifier = node.alias?.value || node.name.value;
pathStack.push(fieldIdentifier);
const isList = listFields.some((field) => field.path.length === pathStack.length && field.path.every((segment, i) => segment === pathStack[i]));
if (isList) {
pathStack.pop();
return null;
}
hasFields = true;
return undefined;
},
leave: () => {
pathStack.pop();
}
} });
return hasFields ? filtered : null;
}
function filterVariables(variables, document) {
if (!variables) return undefined;
const usedVariables = new Set();
(0, graphql.visit)(document, { Variable: (node) => {
usedVariables.add(node.name.value);
} });
const filtered = {};
const varsObj = variables;
for (const key of usedVariables) {
if (key in varsObj) {
filtered[key] = varsObj[key];
}
}
return (0, es_toolkit_compat.isEmpty)(filtered) ? undefined : filtered;
}
/**
* Creates a TheGraph client that supports pagination for list fields
*
* @param theGraphClient - The GraphQL client to use for requests
* @returns A TheGraph client that supports pagination for list fields
* @internal Used internally by createTheGraphClient
*/
function createTheGraphClientWithPagination(theGraphClient) {
async function executeListFieldPagination(document, variables, field, requestHeaders) {
const results = [];
let currentSkip = field.skipValue || 0;
let hasMore = true;
const batchSize = Math.min(field.firstValue || THE_GRAPH_LIMIT, THE_GRAPH_LIMIT);
while (hasMore) {
const query = createSingleFieldQuery(document, field, currentSkip, batchSize);
const existingVariables = filterVariables(variables, query) ?? {};
const response = await theGraphClient.request(query, {
...existingVariables,
first: batchSize,
skip: currentSkip
}, requestHeaders);
const data = (0, es_toolkit_compat.get)(response, field.path) ?? (0, es_toolkit_compat.get)(response, field.fieldName);
const parentPath = field.path.slice(0, -1);
const parentData = (0, es_toolkit_compat.get)(response, parentPath);
if ((0, es_toolkit_compat.isArray)(parentData) && parentData.length > 0) {
throw new Error(`Response is an array, but expected a single object for field ${parentPath.join(".")}. The @fetchAll directive is not supported inside a query that returns a list of items.`);
}
if ((0, es_toolkit_compat.isArray)(data) && data.length > 0) {
results.push(...data);
hasMore = data.length === batchSize;
} else {
hasMore = false;
}
currentSkip += batchSize;
}
return results;
}
return { async query(documentOrOptions, variablesRaw, requestHeadersRaw) {
let document;
let variables;
let requestHeaders;
if (isRequestOptions(documentOrOptions)) {
document = documentOrOptions.document;
variables = documentOrOptions.variables ?? {};
requestHeaders = documentOrOptions.requestHeaders;
} else {
document = documentOrOptions;
variables = variablesRaw ?? {};
requestHeaders = requestHeadersRaw;
}
const { document: processedDocument, fetchAllFields } = stripFetchAllDirective(document);
const listFields = extractFetchAllFields(processedDocument, variables, fetchAllFields);
if (listFields.length === 0) {
return theGraphClient.request(processedDocument, variables, requestHeaders);
}
const result = {};
const sortedFields = (0, es_toolkit.sortBy)(listFields, [(field) => field.path.length]);
const fieldDataPromises = sortedFields.map(async (field) => ({
field,
data: await executeListFieldPagination(processedDocument, variables, field, requestHeaders)
}));
const fieldResults = await Promise.all(fieldDataPromises);
for (const { field, data } of fieldResults) {
(0, es_toolkit_compat.set)(result, field.path, data);
}
const nonListQuery = createNonListQuery(processedDocument, listFields);
if (nonListQuery) {
const nonListResult = await theGraphClient.request(nonListQuery, filterVariables(variables, nonListQuery) ?? {}, requestHeaders);
const merged = customMerge(nonListResult, result);
return merged;
}
return result;
} };
}
function isRequestOptions(args) {
return typeof args === "object" && args !== null && "document" in args;
}
//#endregion
//#region src/thegraph.ts
/**
* Schema for validating client options for the TheGraph client.
*/
const ClientOptionsSchema = zod.z.object({
instances: zod.z.array(__settlemint_sdk_utils_validation.UrlOrPathSchema),
accessToken: __settlemint_sdk_utils_validation.ApplicationAccessTokenSchema.optional(),
subgraphName: zod.z.string(),
cache: zod.z.enum([
"default",
"force-cache",
"no-cache",
"no-store",
"only-if-cached",
"reload"
]).optional()
});
/**
* Constructs the full URL for TheGraph GraphQL API based on the provided options
*
* @param options - The client options for configuring TheGraph client
* @returns The complete GraphQL API URL as a string
* @throws Will throw an error if no matching instance is found for the specified subgraph
*/
function getFullUrl(options) {
const instance = options.instances.find((instance$1) => instance$1.endsWith(`/${options.subgraphName}`));
if (!instance) {
throw new Error(`Instance for subgraph ${options.subgraphName} not found`);
}
return new URL(instance).toString();
}
/**
* Creates a TheGraph GraphQL client with proper type safety using gql.tada
*
* @param options - Configuration options for the client including instance URLs,
* access token and subgraph name
* @param clientOptions - Optional GraphQL client configuration options
* @returns An object containing:
* - client: The configured GraphQL client instance
* - graphql: The initialized gql.tada function for type-safe queries
* @throws Will throw an error if the options fail validation against ClientOptionsSchema
* @example
* import { createTheGraphClient } from '@settlemint/sdk-thegraph';
* import type { introspection } from '@schemas/the-graph-env-kits';
* import { createLogger, requestLogger } from '@settlemint/sdk-utils/logging';
*
* const logger = createLogger();
*
* const { client, graphql } = createTheGraphClient<{
* introspection: introspection;
* disableMasking: true;
* scalars: {
* Bytes: string;
* Int8: string;
* BigInt: string;
* BigDecimal: string;
* Timestamp: string;
* };
* }>({
* instances: JSON.parse(process.env.SETTLEMINT_THEGRAPH_SUBGRAPHS_ENDPOINTS || '[]'),
* accessToken: process.env.SETTLEMINT_ACCESS_TOKEN,
* subgraphName: 'kits'
* }, {
* fetch: requestLogger(logger, "the-graph-kits", fetch) as typeof fetch,
* });
*
* // Making GraphQL queries
* const query = graphql(`
* query SearchAssets {
* assets @fetchAll {
* id
* name
* symbol
* }
* }
* `);
*
* const result = await client.request(query);
*/
function createTheGraphClient(options, clientOptions) {
(0, __settlemint_sdk_utils_runtime.ensureServer)();
const validatedOptions = (0, __settlemint_sdk_utils_validation.validate)(ClientOptionsSchema, options);
const graphql$1 = (0, gql_tada.initGraphQLTada)();
const fullUrl = getFullUrl(validatedOptions);
const client = new graphql_request.GraphQLClient(fullUrl, {
...clientOptions,
headers: (0, __settlemint_sdk_utils_http.appendHeaders)(clientOptions?.headers, { "x-auth-token": validatedOptions.accessToken })
});
const originalRequest = client.request.bind(client);
const paginatedClient = createTheGraphClientWithPagination({ request: originalRequest });
client.request = paginatedClient.query;
return {
client,
graphql: graphql$1
};
}
//#endregion
exports.ClientOptionsSchema = ClientOptionsSchema;
exports.createTheGraphClient = createTheGraphClient;
exports.createTheGraphClientWithPagination = createTheGraphClientWithPagination;
Object.defineProperty(exports, 'readFragment', {
enumerable: true,
get: function () {
return gql_tada.readFragment;
}
});
//# sourceMappingURL=thegraph.cjs.map