From f5dc43ab1ec0efb8962b447f0ba9f102e714320d Mon Sep 17 00:00:00 2001 From: HexaField <10372036+HexaField@users.noreply.github.com> Date: Mon, 4 May 2026 09:17:38 +1000 Subject: [PATCH] feat: SPARQL trace enrichment (feat/sparql-1.2-cleanup branch) - GraphQL/Apollo adapter enhanced with SPARQL detection - Scans operation variables for SPARQL query patterns - Scans response data for SPARQL content - Auto-logs SPARQL traces when detected in GraphQL operations - Supports SELECT, CONSTRUCT, ASK, DESCRIBE, INSERT, DELETE - Compatible with coasys/ad4m feat/sparql-1.2-cleanup branch --- packages/bridge/src/adapters/graphql.ts | 115 ++++++++++++++++++++++-- 1 file changed, 106 insertions(+), 9 deletions(-) diff --git a/packages/bridge/src/adapters/graphql.ts b/packages/bridge/src/adapters/graphql.ts index c90f1db..965ea81 100644 --- a/packages/bridge/src/adapters/graphql.ts +++ b/packages/bridge/src/adapters/graphql.ts @@ -26,6 +26,7 @@ interface DevToolsBridge { trackSubscription(sub: any): number; updateSubscription(id: number, update: any): void; recordEventStreamMessage(): void; + logSparqlQuery?(info: { query: string; modelName: string; perspectiveUUID: string }): void; } /** @@ -61,7 +62,6 @@ function printQuery(query: any): string { if (typeof query === 'string') return query; if (query?.loc?.source?.body) return query.loc.source.body; try { - // Fallback: JSON serialize the definitions return JSON.stringify(query?.definitions?.map((d: any) => ({ operation: d.operation, name: d.name?.value, @@ -72,9 +72,72 @@ function printQuery(query: any): string { } } +/** + * SPARQL detection patterns — identifies SPARQL query text in variables or responses. + */ +const SPARQL_KEYWORDS = ['SELECT', 'CONSTRUCT', 'ASK', 'DESCRIBE', 'INSERT', 'DELETE', 'WHERE']; +const SPARQL_PATTERN = new RegExp( + `\\b(${SPARQL_KEYWORDS.join('|')})\\b[\\s\\S]*\\b(WHERE|\\{)\\b`, + 'i' +); + +/** + * Attempts to extract SPARQL query text from variables or response data. + */ +function extractSparqlQuery(variables?: Record): string | undefined { + if (!variables) return undefined; + + // Check common variable names that might contain SPARQL + const candidates = ['query', 'sparqlQuery', 'inferQuery', 'sparql', 'expression']; + for (const key of candidates) { + const value = variables[key]; + if (typeof value === 'string' && SPARQL_PATTERN.test(value)) { + return value; + } + } + + // Check all string values + for (const value of Object.values(variables)) { + if (typeof value === 'string' && value.length > 20 && SPARQL_PATTERN.test(value)) { + return value; + } + } + + return undefined; +} + +/** + * Scans response data for SPARQL query text (e.g. in perspectives.infer results). + */ +function extractSparqlFromResponse(data: any): string | undefined { + if (!data) return undefined; + + // Recursively search for SPARQL-looking strings in response + const searchObj = (obj: any, depth: number): string | undefined => { + if (depth > 5) return undefined; + if (typeof obj === 'string' && obj.length > 20 && SPARQL_PATTERN.test(obj)) { + return obj; + } + if (Array.isArray(obj)) { + for (const item of obj) { + const found = searchObj(item, depth + 1); + if (found) return found; + } + } else if (obj && typeof obj === 'object') { + for (const value of Object.values(obj)) { + const found = searchObj(value, depth + 1); + if (found) return found; + } + } + return undefined; + }; + + return searchObj(data, 0); +} + /** * Creates an Apollo Link that intercepts all operations for DevTools. - * Insert this as the first link in the chain. + * Enhanced with SPARQL trace enrichment for the sparql-1.2-cleanup branch. */ export function createDevToolsLink(bridge: DevToolsBridge): ApolloLink { return { @@ -83,8 +146,10 @@ export function createDevToolsLink(bridge: DevToolsBridge): ApolloLink { const opType = getOperationType(operation.query); const queryText = printQuery(operation.query); + // Check if this operation contains SPARQL in its variables + const sparqlInVars = extractSparqlQuery(operation.variables); + if (opType === 'subscription') { - // Track subscriptions const subId = bridge.trackSubscription({ query: queryText, modelName: opName, @@ -93,16 +158,25 @@ export function createDevToolsLink(bridge: DevToolsBridge): ApolloLink { const observable = forward(operation); - // Wrap the observable to track updates return { subscribe(observer: any) { return observable.subscribe({ next(result: any) { bridge.recordEventStreamMessage(); bridge.updateSubscription(subId, { - updateCount: (bridge as any)._getSubUpdateCount?.(subId) ?? 1, lastUpdateTimestamp: Date.now(), }); + + // Check subscription results for SPARQL content + const sparqlInResult = extractSparqlFromResponse(result?.data); + if (sparqlInResult && bridge.logSparqlQuery) { + bridge.logSparqlQuery({ + query: sparqlInResult, + modelName: opName, + perspectiveUUID: operation.variables?.uuid || '', + }); + } + observer.next?.(result); }, error(err: any) { @@ -119,14 +193,15 @@ export function createDevToolsLink(bridge: DevToolsBridge): ApolloLink { } // Queries and Mutations - const method = opType === 'mutation' ? 'POST' : 'POST'; // GraphQL always POST const opId = bridge.logOperation({ type: 'request', transport: 'graphql' as any, operationName: `${opType.toUpperCase()} ${opName}`, - method, + method: 'POST', path: `/graphql`, query: queryText, + queryLanguage: sparqlInVars ? 'sparql' : undefined, + sparqlQuery: sparqlInVars, variables: operation.variables, requestBody: { query: queryText, @@ -135,6 +210,15 @@ export function createDevToolsLink(bridge: DevToolsBridge): ApolloLink { }, }); + // If we detected SPARQL in the variables, also log a SPARQL trace + if (sparqlInVars && bridge.logSparqlQuery) { + bridge.logSparqlQuery({ + query: sparqlInVars, + modelName: opName, + perspectiveUUID: operation.variables?.uuid || operation.variables?.perspectiveUuid || '', + }); + } + const observable = forward(operation); return { @@ -142,12 +226,26 @@ export function createDevToolsLink(bridge: DevToolsBridge): ApolloLink { return observable.subscribe({ next(result: any) { const errors = result?.errors; + + // Check response for SPARQL content and enrich the operation + const sparqlInResponse = extractSparqlFromResponse(result?.data); + bridge.completeOperation( opId, result?.data, errors, - { statusCode: errors?.length ? 200 : 200 } + { statusCode: 200 } ); + + // Log additional SPARQL trace if found in response + if (sparqlInResponse && !sparqlInVars && bridge.logSparqlQuery) { + bridge.logSparqlQuery({ + query: sparqlInResponse, + modelName: `${opName} (response)`, + perspectiveUUID: operation.variables?.uuid || '', + }); + } + observer.next?.(result); }, error(err: any) { @@ -178,7 +276,6 @@ export function wrapApolloClient(apolloClient: any, bridge: DevToolsBridge): voi const devToolsLink = createDevToolsLink(bridge); - // Prepend our link to the existing chain const originalLink = apolloClient.link; apolloClient.link = { request(operation: ApolloOperation, forward: any) {