Skip to content

Commit caee7bc

Browse files
authored
feat(api): add saved_query GraphQL field for the curated saved-query library (#7642) (#7652)
Add saved_query(id: String!, params: JSON): JSON to type Query, mirroring GET /api/v1/queries/{id} and the run_saved_query MCP tool by calling the same shared executor (src/saved-queries.mjs runSavedQuery) -- template lookup, param coercion/validation, and execution are all its; nothing re-implemented. Unknown id and invalid params (its not_found / invalid_params toolErrors) map to BAD_USER_INPUT, matching the file's invalid-argument convention; any other executor failure surfaces as a normal GraphQL error. Opaque {query_id, params, data} JSON passthrough per the registry_summary-family convention, RELATIONSHIP_FIELD_COMPLEXITY weight, and tests (template run with a params object literal, unknown id listing valid ids, unknown param, complexity).
1 parent 12fbad9 commit caee7bc

2 files changed

Lines changed: 66 additions & 0 deletions

File tree

src/graphql.mjs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,9 @@ import {
235235
labelsForSs58,
236236
} from "./entity-labels.mjs";
237237
import { loadSudoKey } from "./sudo-key.mjs";
238+
// #7642: saved_query reuses the same maintainer-curated template executor the
239+
// GET /api/v1/queries/{id} route and run_saved_query MCP tool already share.
240+
import { runSavedQuery } from "./saved-queries.mjs";
238241
import { loadNetworkParameters } from "./network-parameters.mjs";
239242
import { loadRandomnessStatus } from "./randomness.mjs";
240243
import { loadAddressMapping, H160_PATTERN } from "./address-mapping.mjs";
@@ -504,6 +507,8 @@ export const SDL = `
504507
curation: JSON
505508
"The discovered candidate-surface ledger: every machine-discovered surface awaiting review, with its subnet (netuid), kind, provider, and review state. Filter by netuid/kind/provider/state and page with limit/cursor, exactly like the REST route. Resolves to {items,total,next_cursor} as opaque JSON. Mirrors GET /api/v1/candidates."
506509
candidates(netuid: Int, kind: String, provider: String, state: String, limit: Int, cursor: String): JSON
510+
"Run one maintainer-curated saved-query template by id, with its template-defined params object -- the same parameterized query library REST and the run_saved_query MCP tool execute. Resolves to {query_id, params, data} as opaque JSON. An unknown id or invalid params is a BAD_USER_INPUT error listing the valid template ids, not a silently substituted default. Mirrors GET /api/v1/queries/{id}."
511+
saved_query(id: String!, params: JSON): JSON
507512
"The recorded response fixtures for registered surfaces, used to replay/verify a surface without calling it. Null when no fixture index has been baked in this environment. Opaque JSON passed through verbatim, matching the list_fixtures MCP/REST shape. Mirrors GET /api/v1/fixtures."
508513
fixtures: JSON
509514
"The agent-callable service catalog: without a netuid, the global index of subnets exposing callable services; with one, that subnet's full per-service catalog. Both are overlaid with live health exactly as REST composes them. Null when the catalog has not been baked. Opaque JSON, matching the get_agent_catalog MCP/REST shape. Mirrors GET /api/v1/agent-catalog."
@@ -4250,6 +4255,7 @@ export const FIELD_COMPLEXITY = {
42504255
agent_resources: RELATIONSHIP_FIELD_COMPLEXITY,
42514256
curation: RELATIONSHIP_FIELD_COMPLEXITY,
42524257
candidates: RELATIONSHIP_FIELD_COMPLEXITY,
4258+
saved_query: RELATIONSHIP_FIELD_COMPLEXITY,
42534259
fixtures: RELATIONSHIP_FIELD_COMPLEXITY,
42544260
agent_catalog: RELATIONSHIP_FIELD_COMPLEXITY,
42554261
freshness: RELATIONSHIP_FIELD_COMPLEXITY,
@@ -6210,6 +6216,28 @@ const rootValue = {
62106216
return loadArtifact(context, "/metagraph/registry-summary.json");
62116217
},
62126218

6219+
async saved_query({ id, params }, context) {
6220+
// #7642: the same maintainer-curated template executor the REST route and
6221+
// run_saved_query MCP tool share (src/saved-queries.mjs) -- template
6222+
// lookup, param coercion/validation, and execution are all its. Its
6223+
// not_found (unknown id) and invalid_params toolErrors map to
6224+
// BAD_USER_INPUT, matching this file's invalid-argument convention; any
6225+
// other executor failure surfaces as a normal GraphQL error.
6226+
try {
6227+
return await runSavedQuery(context.env, id, params ?? {});
6228+
} catch (err) {
6229+
if (
6230+
err?.toolError &&
6231+
(err.code === "not_found" || err.code === "invalid_params")
6232+
) {
6233+
throw new GraphQLError(err.message, {
6234+
extensions: { code: "BAD_USER_INPUT" },
6235+
});
6236+
}
6237+
throw err;
6238+
}
6239+
},
6240+
62136241
source_health(_args, context) {
62146242
// Same baked artifact the REST route + get_source_health MCP tool read.
62156243
return loadArtifact(context, "/metagraph/source-health.json");

tests/graphql.test.mjs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16667,6 +16667,44 @@ describe("graphql — registry_leaderboards (#5661, shared composer + REST-match
1666716667
});
1666816668
});
1666916669

16670+
describe("graphql — saved_query (#7642, curated saved-query templates)", () => {
16671+
test("runs a template through the shared executor and returns the {query_id, params, data} envelope", async () => {
16672+
const { status, body } = await gql(
16673+
`{ saved_query(id: "subnet-leaderboard", params: { limit: 5 }) }`,
16674+
);
16675+
assert.equal(status, 200);
16676+
assert.equal(body.errors, undefined);
16677+
const result = body.data.saved_query;
16678+
assert.equal(result.query_id, "subnet-leaderboard");
16679+
assert.equal(result.params.limit, 5);
16680+
assert.ok(result.data, "expected the executed template's data payload");
16681+
});
16682+
16683+
test("an unknown id is BAD_USER_INPUT listing the valid template ids", async () => {
16684+
const { status, body } = await gql(
16685+
`{ saved_query(id: "no-such-template") }`,
16686+
);
16687+
assert.equal(status, 200);
16688+
assert.ok(body.errors.find((e) => e.extensions?.code === "BAD_USER_INPUT"));
16689+
assert.match(body.errors[0].message, /no-such-template/);
16690+
assert.match(body.errors[0].message, /subnet-leaderboard/);
16691+
assert.equal(body.data?.saved_query ?? null, null);
16692+
});
16693+
16694+
test("an unknown param is BAD_USER_INPUT, not a silent default", async () => {
16695+
const { status, body } = await gql(
16696+
`{ saved_query(id: "subnet-leaderboard", params: { not_a_real_param: 1 }) }`,
16697+
);
16698+
assert.equal(status, 200);
16699+
assert.ok(body.errors.find((e) => e.extensions?.code === "BAD_USER_INPUT"));
16700+
assert.match(body.errors[0].message, /not_a_real_param/);
16701+
});
16702+
16703+
test("is priced at the relationship-field complexity weight", () => {
16704+
assert.equal(FIELD_COMPLEXITY.saved_query, FIELD_COMPLEXITY.candidates);
16705+
});
16706+
});
16707+
1667016708
describe("Query.subnet_hyperparameters / subnet_hyperparameters_history", () => {
1667116709
const HP_FIELDS =
1667216710
"kappa_ratio immunity_period tempo registration_allowed min_burn_tao bonds_moving_avg_raw liquid_alpha_enabled min_childkey_take_ratio";

0 commit comments

Comments
 (0)