fix(api): strip server id prefixes from graphql request variables

This commit is contained in:
Pujit Mehrotra
2024-10-21 12:16:45 -04:00
parent b3d046f4ea
commit 91bcbc3d6f
2 changed files with 76 additions and 29 deletions

View File

@@ -1,31 +1,44 @@
import { type ApolloServerPlugin } from "@apollo/server";
import { getServerIdentifier } from "@app/core/utils/server-identifier";
import { type ApolloServerPlugin } from '@apollo/server';
import { getServerIdentifier } from '@app/core/utils/server-identifier';
import { updateObject } from '@app/utils';
type ObjectModifier = (obj: object) => void;
/**
* Modify all ID fields in the GQL response object to include a prefix
* @param obj GQL response object, to be modified in place
* Returns a function that takes an object and updates any 'id' properties to
* include the given serverId as a prefix.
*
* e.g. If the object is { id: '1234' }, the returned function will update it to
* { id: '<serverId>:1234' }.
*
* @param serverId - The server identifier to use as the prefix.
* @returns A function that takes an object and updates any 'id' properties with the given serverId.
*/
const updateId = (obj: Record<string, unknown>) => {
const serverId = getServerIdentifier();
const stack = [obj];
let iterations = 0;
// Prevent infinite loops
while (stack.length > 0 && iterations < 100) {
const current = stack.pop();
if (current && typeof current === 'object') {
if ('id' in current && typeof current.id === 'string') {
current.id = `${serverId}:${current.id}`;
}
for (const value of Object.values(current)) {
if (value && typeof value === 'object') {
stack.push(value as Record<string, unknown>);
}
}
function prefixWithServerId(serverId: string): ObjectModifier {
return (currentObj) => {
if ('id' in currentObj && typeof currentObj.id === 'string') {
currentObj.id = `${serverId}:${currentObj.id}`;
}
};
}
iterations++;
/**
* Takes an object and removes any server prefix from the 'id' property.
*
* e.g. If the object is { id: '<serverId>:1234' }, the returned function will update it to
* { id: '1234' }.
*
* @param current - The object to update. If it has an 'id' property that is a string and
* has a server prefix, the prefix is removed.
*/
const stripServerPrefixFromIds: ObjectModifier = (current) => {
if ('id' in current && typeof current.id === 'string') {
const parts = current.id.split(':');
// if there are more or less than 2 parts to the split,
// assume there is no server prefix and don't touch it.
if (parts.length === 2) {
current.id = parts[1];
}
}
};
@@ -37,13 +50,16 @@ export const idPrefixPlugin: ApolloServerPlugin = {
}
// If ID is requested, return an ID field with an extra prefix
return {
async didResolveOperation({ request }) {
if (request.variables) {
updateObject(request.variables, stripServerPrefixFromIds)
stripServerPrefixFromIds(request.variables);
}
},
async willSendResponse({ response }) {
if (
response.body.kind === 'single' &&
response.body.singleResult.data
) {
// Iteratively update all ID fields with a prefix
updateId(response.body.singleResult.data);
if (response.body.kind === 'single' && response.body.singleResult.data) {
const serverId = getServerIdentifier();
updateObject(response.body.singleResult.data, prefixWithServerId(serverId));
}
},
};

View File

@@ -61,3 +61,34 @@ export async function batchProcess<Input, T>(items: Input[], action: (id: Input)
errorOccured: errors.length > 0,
};
}
/**
* Traverses an object and its nested objects, passing each one to a callback function.
*
* This function iterates over the input object, using a stack to keep track of nested objects,
* and applies the given modifier function to each object it encounters.
* It prevents infinite loops by limiting the number of iterations.
*
* @param obj - The object to be traversed and modified.
* @param modifier - A callback function, taking an object. Modifications should happen in place.
*/
export function updateObject(obj: object, modifier: (currentObj: object) => void) {
const stack = [obj];
let iterations = 0;
// Prevent infinite loops
while (stack.length > 0 && iterations < 100) {
const current = stack.pop();
if (current && typeof current === 'object') {
modifier(current);
for (const value of Object.values(current)) {
if (value && typeof value === 'object') {
stack.push(value);
}
}
}
iterations++;
}
}