Skip to main content

Файловые источники данных в Graph Node

Высокоуровневое описание

Был введен новый "вид" источника данных: file/ipfs. Эти источники данных могут быть определены статически, на основе адресуемого хэша содержимого, или могут быть созданы динамически с помощью сопоставлений на основе цепочки.

При инстанцировании Graph Node попытается получить соответствующие данные из IPFS, основываясь на заявленной доступности файла для цепочки доступности, если она настроена. Если файл будет получен, будет запущен назначенный обработчик. Этот обработчик сможет сохранять только сущности, указанные для источника данных в манифесте, и эти сущности не будут редактироваться обработчиками основной цепочки. Доступность этих сущностей для запроса будет определяться цепочкой доступности, если она настроена.

Подробная спецификация

Пример представляет собой подграф со следующей информацией о магазине:

type Store @entity {
id: ID!
owner: Bytes!
block: BigInt!
timestamp: BigInt!
storage: String!
productsCount: BigInt!
productIds: [String!]!
ordersCount: BigInt!
products: [Product!] @derivedFrom(field: "store")
vendors: [Bytes!]!
collections: [Collection!] @derivedFrom(field: "store")
collectionIds: [String!]!
rating: BigInt!
name: String
slogan: String
slug: String
link: String
description: String
deliveryCountries: [DeliveryCountry!]!
}

В данном случае name, slogan, slug, link и description находятся в файле IPFS, а ID и другая информация получена из Ethereum. Это разделение и объединение во время запроса определено в подграфе schema.graphql следующим образом:

type Store @entity {
id: ID!
owner: Bytes!
block: BigInt!
timestamp: BigInt!
storage: String!
productsCount: BigInt!
productIds: [String!]!
ordersCount: BigInt!
products: [Product!] @derivedFrom(field: "store")
vendors: [Bytes!]!
collections: [Collection!] @derivedFrom(field: "store")
collectionIds: [String!]!
rating: BigInt!
metadata: StoreMetadata
deliveryCountries: [DeliveryCountry!]!
}

type StoreMetadata @entity {
id: ID!
name: String
slogan: String
slug: String
link: String
description: String
storage: String
entityId: String
}

Теперь у нас есть сущность (StoreMetadata), которая зависит только от IPFS . Поэтому ее можно рассматривать отдельно для доказательства индексации.

Статический источник данных файла объявляется как:

- name: StoreMetadataTemplate
kind: file/ipfs
mapping:
apiVersion: 0.0.7
language: wasm/assemblyscript
file: ./mappings/metadataHandlers/StoreMetadataHandler.ts
handler: handleStoreMetadata
entities:
- StoreMetadata
abis:
- name: Melcor
file: ./abis/Melcor.json
  • В этом случае ipfs является типом file, указывая на то, что этот файл будет храниться в сети IPFS.
  • Сущности, указанные в отображении, важны для обеспечения изоляции - сущности, указанные в источниках данных файлов, не должны быть доступны другим источникам данных (а сам источник данных файлов должен только создавать эти сущности). Это должно проверяться во время компиляции, а также нарушаться во время выполнения, если API store используется для обновления файлового источника данных.
  • Для каждого источника файловых данных может быть только один обработчик.

В нашем примере источник данных будет не статическим, а шаблоном, в этом случае поле source будет опущено. Для создания шаблона можно использовать текущий API create:

export function SetStoreHandler(event: SetStore): void {
const id = event.params.storeId.toString();
let store = Store.load(id);
if (store === null) {
store = new Store(id);

store.owner = event.params.owner;
store.vendors = [event.params.owner];
store.productsCount = zeroBigInt;
store.ordersCount = zeroBigInt;
store.block = event.block.number;
store.rating = zeroBigInt;
store.timestamp = event.block.timestamp;
store.productIds = [];
store.collectionIds = [];
}

const countries: string[] = [];
for (let i = 0; i < event.params.deliveryCountries.length; i++) {
const country = event.params.deliveryCountries[i].toString();
countries.push(country);
}
store.deliveryCountries = countries;

store.storage = event.params.metadata;
const metadata = store.storage + "/metadata.json";
store.metadata = metadata;

const context = new DataSourceContext();
context.setString("entityId", id);
context.setString("storage", store.storage);
StoreMetadataTemplate.createWithContext(metadata, context);
store.save();

let user = User.load(event.params.owner.toHex());
if (user === null) {
user = createUser(
event.params.owner,
event.block.number,
event.block.timestamp,
zeroBigInt
);
}
user.save();
}

Обработчик файлов выглядит следующим образом:

export function handleStoreMetadata(content: Bytes): void {
const storeMetadata = new StoreMetadata(dataSource.stringParam());
const context = dataSource.context();
const entityId = context.getString("entityId");
const storage = context.getString("storage");
const try_value = json.try_fromBytes(content);

storeMetadata.entityId = entityId.toString();
storeMetadata.storage = storage;
if (try_value.isOk) {
const value = try_value.value;

if (value.kind == JSONValueKind.OBJECT) {
const jsonData = value.toObject();
const name = jsonData.get("name");
const description = jsonData.get("description");
const slogan = jsonData.get("slogan");
const slug = jsonData.get("slug");
const link = jsonData.get("link");

if (name) {
storeMetadata.name = name.toString();
}

if (slogan) {
storeMetadata.slogan = slogan.toString();
}

if (description) {
storeMetadata.description = description.toString();
}

if (slug) {
storeMetadata.slug = slug.toString();
}

if (link) {
storeMetadata.link = link.toString();
}
}

storeMetadata.save();
}
}

Обратите внимание, что в этом случае сопоставление сущности на базе Ethereum с сущностью на базе IPFS происходит полностью в сопоставлении данных Ethereum, что позволяет нескольким сущностям Ethereum ссылаться на одну и ту же сущность на базе файла.

Идентичный источник данных файла может быть создан более одного раза (например, если несколько ERC721 имеют один и тот же tokenURI). В этом случае соответствующий обработчик файлов должен быть запущен только один раз.

Индексирование источников данных IPFS

Поведение индексирования будет зависеть от того, был ли Graph Node сконфигурирован с цепочкой доступности.

Без цепочки доступности

При отсутствии цепочки доступности, когда создается файловый источник данных, Graph Node пытается найти этот файл с настроенного шлюза IPFS узла. Если Graph Node не может найти файл, он должен повторить попытку несколько раз, отступая назад со временем. После нахождения файла Graph Node выполнит связанный с ним обработчик, обновляя хранилище. Обновления связанных сущностей не будут частью PoI подграфа.

С цепочкой доступности

Первоначальная реализация не будет включать цепочку доступности.

Если файл отмечен как доступный в цепочке доступности в последнем блоке, и Graph Node может найти этот файл, он должен обработать соответствующий обработчик и обновить PoI для обновленных сущностей с помощью последнего блока цепочки доступности. Затем он должен прослушать обновления доступности этого файла, и если файл помечен как недоступный, диапазон блоков доступности результирующих сущностей должен быть закрыт по состоянию на последний блок, а PoI обновлен.

Если файл помечен как доступный цепочкой доступности по последнему блоку, а Graph Node не может найти этот файл, он должен указать цепочке доступности, что не может найти файл, и периодически повторять попытки найти файл.

Если файл не отслеживается цепочкой доступности или помечен как недоступный, а Graph Node может найти файл, он должен указать цепочке доступности, что файл доступен. Примечательно, что Graph Node не должен выполнять соответствующий обработчик, пока Цепочка доступности не пометит файл как Доступный.

Если файл не отслеживается Цепочкой доступности или помечен как Недоступный, и Graph Node не может найти файл, он должен указать Цепочке доступности, что файл недоступен, и периодически повторять попытки найти файл.

Взаимодействие с хранилищем

  • Сопоставления файловых источников данных могут загружать сущности только из сущностей цепочечных источников данных, вплоть до блока создания цепочки файловых источников данных.
  • Сущности цепочки источников данных не могут каким-либо образом взаимодействовать с сущностями файловых источников данных.

Запрос подграфов с использованием файловых источников данных

Во время запроса Graph Node должен знать, запрашивает ли запрос данные, включающие сущности файловых источников данных.

Пример:

## No file-based data source entities:
fragment StoreInfo on Store {
id
storage
owner
block
timestamp
productsCount
ordersCount
rating
name
description
deliveryCountries {
id
name
}
}

## Including data from file-based entities:
fragment StoreInfo on Store {
id
storage
owner
block
timestamp
productsCount
ordersCount
rating
metadata {
name
description
}
deliveryCountries {
id
metadata {
name
}
}
}

query getStoreById($id: ID!, $tokens: [String!]) {
store(id: $id) {
...StoreInfo
}
}
  • Если запрос требует данные из файлового источника данных, а в Graph Node настроена цепочка доступности, то в дополнение к хэшу блока Ethereum запрос должен будет предоставить хэш блока цепочки доступности.
  • Если хэш блока Availability Chain не предоставлен, то по умолчанию будет использоваться самый последний из известных Graph Node, аналогично текущему обращению с хэшами блоков Ethereum в Graph Node. Обратите внимание, что такой запрос не является детерминированным.
  • Graph Node может быть не в состоянии поддерживать все блоки Availability Chain, исходя из того, когда он начал индексирование, и должен отказаться обслуживать запросы, если указанная Availability Chain находится вне диапазона.