Skip to content
Open
Show file tree
Hide file tree
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
79 changes: 79 additions & 0 deletions indexer/distributions/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,7 +1,86 @@
enum DistributionStatus {
ACTIVE
PAUSED
COMPLETED
CANCELLED
}

type DistributionBatch {
id: ID!
contractId: String!
distributor: String!
token: String!
totalAmount: String!
claimedAmount: String!
recipientCount: Int!
status: DistributionStatus!
pausedAt: String
resumedAt: String
uniqueRef: String!
ledgerNumber: Int!
txHash: String!
claims(pagination: PaginationInput): ClaimConnection!
createdAt: String!
updatedAt: String!
}

type ClaimAction {
id: ID!
batchId: ID!
claimant: String!
amount: String!
txHash: String!
ledgerNumber: Int!
eventTimestamp: String!
createdAt: String!
}

input DistributionFilterInput {
distributor: String
token: String
status: DistributionStatus
contractId: String
}

input ClaimFilterInput {
batchId: ID
claimant: String
}

input PaginationInput {
first: Int
after: String
}

type DistributionConnection {
nodes: [DistributionBatch!]!
pageInfo: PageInfo!
totalCount: Int!
}

type ClaimConnection {
nodes: [ClaimAction!]!
pageInfo: PageInfo!
totalCount: Int!
}

type PageInfo {
hasNextPage: Boolean!
endCursor: String
}

type Query {
distributionBatch(id: ID!): DistributionBatch
distributionBatches(
filter: DistributionFilterInput
pagination: PaginationInput
): DistributionConnection!
distributionBatchesByDistributor(
distributor: String!
pagination: PaginationInput
): DistributionConnection!
claimAction(id: ID!): ClaimAction
claims(filter: ClaimFilterInput, pagination: PaginationInput): ClaimConnection!
claimsByClaimant(claimant: String!, pagination: PaginationInput): ClaimConnection!
claimsByBatch(batchId: ID!, pagination: PaginationInput): ClaimConnection!
}
91 changes: 91 additions & 0 deletions indexer/distributions/src/schema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, test } from "vitest";

const __dirname = dirname(fileURLToPath(import.meta.url));
const schemaPath = join(__dirname, "../schema.graphql");

describe("distributions GraphQL schema", () => {
const schemaContent = readFileSync(schemaPath, "utf-8");

test("defines DistributionStatus enum", () => {
expect(schemaContent).toContain("enum DistributionStatus {");
expect(schemaContent).toContain("ACTIVE");
expect(schemaContent).toContain("PAUSED");
expect(schemaContent).toContain("COMPLETED");
expect(schemaContent).toContain("CANCELLED");
});

const definitionBody = (kind: "type" | "input" | "enum", name: string) => {
const match = schemaContent.match(new RegExp(`${kind}\\s+${name}\\s*\\{([\\s\\S]*?)\\n\\}`));
expect(match).not.toBeNull();
return match?.[1] ?? "";
};
Comment on lines +20 to +24

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Parse the SDL instead of regex-matching raw text.

This helper lets the suite pass on malformed schema text as long as the expected substrings still exist somewhere in the file. That means syntax errors, duplicate definitions, or invalid field declarations can slip through here. Please build/parse the schema once and assert on the resulting type map/AST instead of extracting blocks with regex.

Suggested direction
+import { buildSchema, isEnumType, isInputObjectType, isObjectType } from "graphql";
+
 describe("distributions GraphQL schema", () => {
   const schemaContent = readFileSync(schemaPath, "utf-8");
+  const schema = buildSchema(schemaContent);
-
-  const definitionBody = (kind: "type" | "input" | "enum", name: string) => {
-    const match = schemaContent.match(new RegExp(`${kind}\\s+${name}\\s*\\{([\\s\\S]*?)\\n\\}`));
-    expect(match).not.toBeNull();
-    return match?.[1] ?? "";
-  };
 
   test("defines DistributionBatch type aligned with database schema", () => {
-    const body = definitionBody("type", "DistributionBatch");
-    expect(body).toContain("id: ID!");
+    const type = schema.getType("DistributionBatch");
+    expect(isObjectType(type)).toBe(true);
+    expect(type?.getFields().id.type.toString()).toBe("ID!");
   });
🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 20-20: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(${kind}\\s+${name}\\s*\\{([\\s\\S]*?)\\n\\})
Note: [CWE-1333] Inefficient Regular Expression Complexity

(regexp-from-variable)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@indexer/distributions/src/schema.test.ts` around lines 20 - 24, The helper in
schema.test.ts is relying on regex over raw SDL, which can miss malformed or
invalid schema content; replace this in definitionBody with parsing/building the
schema once and asserting against the resulting AST or type map. Use the
existing schemaContent-driven test setup to locate the relevant type/input/enum
definitions by name from the parsed structure instead of matching text blocks,
so syntax errors, duplicates, and invalid fields are actually detected.


test("defines DistributionBatch type aligned with database schema", () => {
const body = definitionBody("type", "DistributionBatch");
expect(body).toContain("id: ID!");
expect(body).toContain("contractId: String!");
expect(body).toContain("distributor: String!");
expect(body).toContain("token: String!");
expect(body).toContain("totalAmount: String!");
expect(body).toContain("claimedAmount: String!");
expect(body).toContain("recipientCount: Int!");
expect(body).toContain("status: DistributionStatus!");
expect(body).toContain("pausedAt: String");
expect(body).toContain("resumedAt: String");
expect(body).toContain("uniqueRef: String!");
expect(body).toContain("ledgerNumber: Int!");
expect(body).toContain("txHash: String!");
expect(body).toContain("claims(pagination: PaginationInput): ClaimConnection!");
expect(body).toContain("createdAt: String!");
expect(body).toContain("updatedAt: String!");
});

test("defines ClaimAction type aligned with database schema", () => {
const body = definitionBody("type", "ClaimAction");
expect(body).toContain("id: ID!");
expect(body).toContain("batchId: ID!");
expect(body).toContain("claimant: String!");
expect(body).toContain("amount: String!");
expect(body).toContain("txHash: String!");
expect(body).toContain("ledgerNumber: Int!");
expect(body).toContain("eventTimestamp: String!");
expect(body).toContain("createdAt: String!");
});

test("defines filter and pagination inputs", () => {
expect(schemaContent).toContain("input DistributionFilterInput {");
expect(schemaContent).toContain("input ClaimFilterInput {");
expect(schemaContent).toContain("input PaginationInput {");
});

test("defines connection and page info types", () => {
expect(schemaContent).toContain("type DistributionConnection {");
expect(schemaContent).toContain("type ClaimConnection {");
expect(schemaContent).toContain("type PageInfo {");
});

test("defines root Query queries", () => {
const normalized = schemaContent.replace(/\s+/g, " ");
expect(normalized).toContain("type Query {");
expect(normalized).toContain("distributionBatch(id: ID!): DistributionBatch");
expect(normalized).toContain(
"distributionBatches( filter: DistributionFilterInput pagination: PaginationInput ): DistributionConnection!",
);
expect(normalized).toContain(
"distributionBatchesByDistributor( distributor: String! pagination: PaginationInput ): DistributionConnection!",
);
expect(normalized).toContain("claimAction(id: ID!): ClaimAction");
expect(normalized).toContain(
"claims(filter: ClaimFilterInput, pagination: PaginationInput): ClaimConnection!",
);
expect(normalized).toContain(
"claimsByClaimant(claimant: String!, pagination: PaginationInput): ClaimConnection!",
);
expect(normalized).toContain(
"claimsByBatch(batchId: ID!, pagination: PaginationInput): ClaimConnection!",
);
});
});