Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 106 additions & 9 deletions packages/bridge/src/adapters/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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,
Expand All @@ -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, any>): 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 {
Expand All @@ -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,
Expand All @@ -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) {
Expand All @@ -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,
Expand All @@ -135,19 +210,42 @@ 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 {
subscribe(observer: any) {
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) {
Expand Down Expand Up @@ -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) {
Expand Down