UNPKG

@settlemint/sdk-thegraph

Version:

TheGraph integration module for SettleMint SDK, enabling querying and indexing of blockchain data through subgraphs

461 lines (457 loc) • 16.1 kB
/* 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 let __settlemint_sdk_utils_http = require("@settlemint/sdk-utils/http"); __settlemint_sdk_utils_http = __toESM(__settlemint_sdk_utils_http); let __settlemint_sdk_utils_runtime = require("@settlemint/sdk-utils/runtime"); __settlemint_sdk_utils_runtime = __toESM(__settlemint_sdk_utils_runtime); let __settlemint_sdk_utils_validation = require("@settlemint/sdk-utils/validation"); __settlemint_sdk_utils_validation = __toESM(__settlemint_sdk_utils_validation); let gql_tada = require("gql.tada"); gql_tada = __toESM(gql_tada); let graphql_request = require("graphql-request"); graphql_request = __toESM(graphql_request); let zod = require("zod"); zod = __toESM(zod); let es_toolkit = require("es-toolkit"); es_toolkit = __toESM(es_toolkit); let es_toolkit_compat = require("es-toolkit/compat"); es_toolkit_compat = __toESM(es_toolkit_compat); let graphql = require("graphql"); graphql = __toESM(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, 10); } 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, 10); } 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) { 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; } return undefined; }, leave: (node) => { pathStack.pop(); if (node.selectionSet && node.selectionSet.selections.length === 0) { return null; } return undefined; } } }); return filtered; } function countExecutableFields(document) { let fieldCount = 0; (0, graphql.visit)(document, { Field: (node) => { if (!node.name.value.startsWith("__")) { if (!node.selectionSet || node.selectionSet.selections.length > 0) { fieldCount += 1; } } } }); return fieldCount; } 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) { if (countExecutableFields(nonListQuery) === 0) { return result; } 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