Repository: graphprotocol/graph-tooling
Pull request #1934 (2025-01-31 11:11:51)

fix: generate types for mappings when int8 is used as id

changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: aa7ae128d862291b7b1113dc73361dbe0984cb77

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/graphprotocol/graph-tooling/new/shiyasmohd/int8-as-id-types?filename=.changeset/new-rules-bake.md&value=---%0A%22%40graphprotocol%2Fgraph-cli%22%3A%20patch%0A---%0A%0Afix%3A%20generate%20types%20for%20mappings%20when%20int8%20is%20used%20as%20id%0A)


cloudflare-workers-and-pages[bot]: ## Deploying graph-tooling with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>aa7ae12</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://ecb14cdd.graph-tooling.pages.dev'>https://ecb14cdd.graph-tooling.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://shiyasmohd-int8-as-id-types.graph-tooling.pages.dev'>https://shiyasmohd-int8-as-id-types.graph-tooling.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/?to=/:account/pages/view/graph-tooling/ecb14cdd-5608-4de1-beec-7669a674ece4)

github-actions[bot]: <!-- changesetsSnapshotPrCommentKey -->
### 🚀 Snapshot Release (`alpha`)

The latest changes of this PR are available as `alpha` on npm (based on the declared `changesets`):
| Package | Version | Info |
|------|---------|----|
| `@graphprotocol/graph-cli` | `0.96.0-alpha-20250131111227-aa7ae128d862291b7b1113dc73361dbe0984cb77` | [npm ↗︎](https://www.npmjs.com/package/@graphprotocol/graph-cli/v/0.96.0-alpha-20250131111227-aa7ae128d862291b7b1113dc73361dbe0984cb77) [unpkg ↗︎](https://unpkg.com/browse/@graphprotocol/graph-cli@0.96.0-alpha-20250131111227-aa7ae128d862291b7b1113dc73361dbe0984cb77/) |
YaroShkvorets: Is this PR finished? 
`graph build` with these changes doesn't work if `id` type is set to `Int8`.
Codegen needs to be adjusted.
shiyasmohd: @YaroShkvorets PR is finished now
YaroShkvorets: @shiyasmohd 
Codegen now works, but scaffolded subgraph doesn't.

Steps to reproduct:
1. Init new subgraph: 
```
graph init test test --from-contract 0xdAC17F958D2ee523a2206206994597C13D831ec7 --protocol ethereum --network mainnet --index-events
```
2. Replace `id: Bytes!` for `Issue` entity in the GraphQL schema with `id: Int8!`
3. Run `graph codegen` - works
4. Run `graph build` - fails with 
```
 Compile subgraphERROR TS2322: Type '~lib/@graphprotocol/graph-ts/common/collections/Bytes' is not assignable to type 'i64'.

     event.transaction.hash.concatI32(event.logIndex.toI32())
     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 in src/contract.ts(30,5)

```

Basically need to generated
```

shiyasmohd: @YaroShkvorets PR is ready for review


Patch:
diff --git a/.changeset/new-rules-bake.md b/.changeset/new-rules-bake.md
new file mode 100644
index 00000000..93c2044c
--- /dev/null
+++ b/.changeset/new-rules-bake.md
@@ -0,0 +1,5 @@
+---
+"@graphprotocol/graph-cli": patch
+---
+
+fix: generate types for mappings when int8 is used as id
diff --git a/packages/cli/src/codegen/schema.ts b/packages/cli/src/codegen/schema.ts
index ef98482a..3bbcd5c9 100644
--- a/packages/cli/src/codegen/schema.ts
+++ b/packages/cli/src/codegen/schema.ts
@@ -16,8 +16,9 @@ import * as util from './util.js';
 class IdField {
   static BYTES = Symbol('Bytes');
   static STRING = Symbol('String');
+  static INT8 = Symbol('Int8');
 
-  private kind: typeof IdField.BYTES | typeof IdField.STRING;
+  private kind: typeof IdField.BYTES | typeof IdField.INT8 | typeof IdField.STRING;
 
   constructor(idField: FieldDefinitionNode | undefined) {
     if (idField?.type.kind !== 'NonNullType') {
@@ -27,15 +28,46 @@ class IdField {
       throw Error('id field must be a named type');
     }
     const typeName = idField.type.type.name.value;
-    this.kind = typeName === 'Bytes' ? IdField.BYTES : IdField.STRING;
+    switch (typeName) {
+      case 'Bytes':
+        this.kind = IdField.BYTES;
+        break;
+      case 'Int8':
+        this.kind = IdField.INT8;
+        break;
+      case 'String':
+        this.kind = IdField.STRING;
+        break;
+      default:
+        this.kind = IdField.STRING;
+        break;
+    }
   }
 
   typeName() {
-    return this.kind === IdField.BYTES ? 'Bytes' : 'string';
+    switch (this.kind) {
+      case IdField.BYTES:
+        return 'Bytes';
+      case IdField.INT8:
+        return 'Int8';
+      case IdField.STRING:
+        return 'string';
+      default:
+        return 'string';
+    }
   }
 
   gqlTypeName() {
-    return this.kind === IdField.BYTES ? 'Bytes' : 'String';
+    switch (this.kind) {
+      case IdField.BYTES:
+        return 'Bytes';
+      case IdField.INT8:
+        return 'Int8';
+      case IdField.STRING:
+        return 'String';
+      default:
+        return 'String';
+    }
   }
 
   tsNamedType() {
@@ -43,19 +75,55 @@ class IdField {
   }
 
   tsValueFrom() {
-    return this.kind === IdField.BYTES ? 'Value.fromBytes(id)' : 'Value.fromString(id)';
+    switch (this.kind) {
+      case IdField.BYTES:
+        return 'Value.fromBytes(id)';
+      case IdField.INT8:
+        return 'Value.fromI64(id)';
+      case IdField.STRING:
+        return 'Value.fromString(id)';
+      default:
+        return 'Value.fromString(id)';
+    }
   }
 
   tsValueKind() {
-    return this.kind === IdField.BYTES ? 'ValueKind.BYTES' : 'ValueKind.STRING';
+    switch (this.kind) {
+      case IdField.BYTES:
+        return 'ValueKind.BYTES';
+      case IdField.INT8:
+        return 'ValueKind.INT8';
+      case IdField.STRING:
+        return 'ValueKind.STRING';
+      default:
+        return 'ValueKind.STRING';
+    }
   }
 
   tsValueToString() {
-    return this.kind == IdField.BYTES ? 'id.toBytes().toHexString()' : 'id.toString()';
+    switch (this.kind) {
+      case IdField.BYTES:
+        return 'id.toBytes().toHexString()';
+      case IdField.INT8:
+        return 'id.toI64().toString()';
+      case IdField.STRING:
+        return 'id.toString()';
+      default:
+        return 'id.toString()';
+    }
   }
 
   tsToString() {
-    return this.kind == IdField.BYTES ? 'id.toHexString()' : 'id';
+    switch (this.kind) {
+      case IdField.BYTES:
+        return 'id.toHexString()';
+      case IdField.INT8:
+        return 'id.toString()';
+      case IdField.STRING:
+        return 'id';
+      default:
+        return 'id';
+    }
   }
 
   static fromFields(fields: readonly FieldDefinitionNode[] | undefined) {
@@ -92,6 +160,7 @@ export default class SchemaCodeGenerator {
           'Bytes',
           'BigInt',
           'BigDecimal',
+          'Int8',
         ],
         '@graphprotocol/graph-ts',
       ),
diff --git a/packages/ts/common/value.ts b/packages/ts/common/value.ts
index ce2a576e..8f7a2959 100644
--- a/packages/ts/common/value.ts
+++ b/packages/ts/common/value.ts
@@ -339,6 +339,14 @@ export class Value {
     return Value.fromArray(output);
   }
 
+  static fromI64Array(input: Array<i64>): Value {
+    const output = new Array<Value>(input.length);
+    for (let i: i64 = 0; i < input.length; i++) {
+      output[i] = Value.fromI64(input[i]);
+    }
+    return Value.fromArray(output);
+  }
+
   static fromBigIntArray(input: Array<BigInt>): Value {
     const output = new Array<Value>(input.length);
     for (let i: i32 = 0; i < input.length; i++) {


Test patch:
diff --git a/packages/cli/src/codegen/schema.test.ts b/packages/cli/src/codegen/schema.test.ts
index cbf3002c..fd1da33f 100644
--- a/packages/cli/src/codegen/schema.test.ts
+++ b/packages/cli/src/codegen/schema.test.ts
@@ -583,6 +583,134 @@ describe('Schema code generator', { concurrent: true }, () => {
     });
   });
 
+  test('Should handle references with Int8 id types', async () => {
+    const codegen = createSchemaCodeGen(`
+    interface Employee {
+      id: Int8!
+      name: String!
+     }
+
+    type Worker implements Employee @entity {
+      id: Int8!
+      name: String!
+      tasks: [Task!]
+   }
+
+    type Task @entity {
+      id: Int8!
+      employee: Employee!
+      workers: [Worker!] @derivedFrom(field: "tasks")
+      worker: Worker!
+   }
+`);
+
+    const generatedTypes = codegen.generateTypes();
+    await testEntity(generatedTypes, {
+      name: 'Task',
+      members: [],
+      methods: [
+        {
+          name: 'constructor',
+          params: [new Param('id', new NamedType('Int8'))],
+          returnType: undefined,
+          body: "\n      super()\n      this.set('id', Value.fromI64(id))\n      ",
+        },
+        {
+          name: 'save',
+          params: [],
+          returnType: new NamedType('void'),
+          body:
+            '\n' +
+            "        let id = this.get('id')\n" +
+            '        assert(id != null,\n' +
+            "               'Cannot save Task entity without an ID')\n" +
+            '        if (id) {\n' +
+            '          assert(id.kind == ValueKind.INT8,\n' +
+            "                 `Entities of type Task must have an ID of type Int8 but the id '${id.displayData()}' is of type ${id.displayKind()}`)\n" +
+            "          store.set('Task', id.toI64().toString(), this)\n" +
+            '        }',
+        },
+        {
+          name: 'loadInBlock',
+          static: true,
+          params: [new Param('id', new NamedType('Int8'))],
+          returnType: new NullableType(new NamedType('Task')),
+          body:
+            '\n' +
+            "        return changetype<Task | null>(store.get_in_block('Task', id.toString()))\n" +
+            '        ',
+        },
+        {
+          name: 'load',
+          static: true,
+          params: [new Param('id', new NamedType('Int8'))],
+          returnType: new NullableType(new NamedType('Task')),
+          body:
+            '\n' +
+            "        return changetype<Task | null>(store.get('Task', id.toString()))\n" +
+            '        ',
+        },
+        {
+          name: 'get id',
+          params: [],
+          returnType: new NamedType('i64'),
+          body: `let value = this.get("id")
+            if (!value || value.kind == ValueKind.NULL) {
+               return 0;
+             } else {
+               return value.toI64()
+             }`,
+        },
+        {
+          name: 'set id',
+          params: [new Param('value', new NamedType('i64'))],
+          returnType: undefined,
+          body: "\n      this.set('id', Value.fromI64(value))\n    ",
+        },
+        {
+          name: 'get employee',
+          params: [],
+          returnType: new NamedType('i64'),
+          body: `let value = this.get('employee')
+            if (!value || value.kind == ValueKind.NULL) {
+              return 0;
+            } else {
+              return value.toI64()
+            }`,
+        },
+        {
+          name: 'set employee',
+          params: [new Param('value', new NamedType('i64'))],
+          returnType: undefined,
+          body: "\n      this.set('employee', Value.fromI64(value))\n    ",
+        },
+        {
+          name: 'get worker',
+          params: [],
+          returnType: new NamedType('i64'),
+          body: `let value = this.get('worker')
+          if (!value || value.kind == ValueKind.NULL) {
+            return 0;
+          } else {
+            return value.toI64()
+          }`,
+        },
+        {
+          name: 'set worker',
+          params: [new Param('value', new NamedType('i64'))],
+          returnType: undefined,
+          body: "\n      this.set('worker', Value.fromI64(value))\n    ",
+        },
+        {
+          name: 'get workers',
+          params: [],
+          returnType: new NamedType('WorkerLoader'),
+          body: "\n      return new WorkerLoader('Task', this.get('id')!.toString(), 'workers')\n    ",
+        },
+      ],
+    });
+  });
+
   test('get related method for WithBytes entity', async () => {
     const codegen = createSchemaCodeGen(`
       type WithBytes @entity {