Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support subqery _metadata query #17

Merged
merged 7 commits into from
Oct 16, 2024
Merged
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Support subqery _metadata query(#4)
- GraphiQL control flag - `playground`
- Health check API `/.well-known/apollo/server-health`

### Fixed
- Bigint query type not match

## [0.1.0] - 2024-09-20
### Fixed
- Package rename @subql/query-subgraph
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
},
"dependencies": {
"@graphile/simplify-inflection": "^8.0.0-beta.5",
"@subql/utils": "^2.14.0",
"@types/yargs": "^17.0.33",
"dotenv": "^16.4.5",
"eslint": "^8.8.0",
"express": "^4.21.1",
"postgraphile": "^5.0.0-beta.28",
"yargs": "latest"
},
Expand All @@ -32,6 +34,7 @@
"@apollo/client": "3.0.0",
"@geut/chan": "^3.2.9",
"@tsconfig/node20": "^20.1.4",
"@types/express": "^4",
"@types/jest": "^29.5.12",
"@typescript-eslint/eslint-plugin": "^5",
"@typescript-eslint/parser": "5",
Expand Down
11 changes: 10 additions & 1 deletion src/config/graphile.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import { ArgFilterPlugin } from '../plugins/filter/ArgFilterPlugin';
import { OrderByAttributesPlugin } from '../plugins/filter/OrderByAttributesPlugin';
import { CreateMetadataPlugin } from '../plugins/GetMetadataPlugin';
import { CreateSubqueryMetadataPlugin } from '../plugins/GetSubqueryMetadataPlugin';
import historicalPlugins from '../plugins/historical';
import { OffsetToSkipPlugin } from '../plugins/OffsetToSkipPlugin';
import { PgIdToNodeIdPlugin } from '../plugins/PgIdToNodeIdPlugin';
Expand All @@ -23,7 +24,7 @@

dotenv.config();

export function genPreset(args: ArgsInterface) {

Check warning on line 27 in src/config/graphile.config.ts

View workflow job for this annotation

GitHub Actions / code-style

Missing return type on function
const DEFAULT_PORT = 3000;
const pgConnection = util.format(
'postgres://%s:%s@%s:%s/%s',
Expand All @@ -37,9 +38,16 @@

const SchemaSmartTagsPlugin = CreateSchemaSmartTagsPlugin(pgSchema);
const metadataPlugin = CreateMetadataPlugin(pgSchema);
const subqueryMetadataPlugin = CreateSubqueryMetadataPlugin(pgSchema, args);
const preset: GraphileConfig.Preset = {
extends: [PostGraphileAmberPreset, PgSimplifyInflectionPreset],
grafserv: { port: DEFAULT_PORT },
grafserv: {
port: DEFAULT_PORT,
graphiql: args.playground,
graphiqlPath: '/graphiql',
graphiqlOnGraphQLGET: false,
graphqlPath: '/',
},
pgServices: [
makePgService({
connectionString: pgConnection,
Expand All @@ -62,6 +70,7 @@
SchemaSmartTagsPlugin,
...historicalPlugins,
metadataPlugin,
subqueryMetadataPlugin,
PgRowByVirtualIdPlugin,
PgIdToNodeIdPlugin,
ArgFilterPlugin,
Expand Down
1 change: 1 addition & 0 deletions src/config/yargs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const yargsOptions = {
demandOption: false,
describe: 'Enable graphql playground',
type: 'boolean',
default: false,
},
// TODO
'query-limit': {
Expand Down
1 change: 1 addition & 0 deletions src/plugins/GetMetadataPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@

type MetaEntry = { key: string; value: MetaType };

export function CreateMetadataPlugin(schemaName: string) {

Check warning on line 30 in src/plugins/GetMetadataPlugin.ts

View workflow job for this annotation

GitHub Actions / code-style

Missing return type on function
return makeExtendSchemaPlugin((build) => {
// TODO Only handled the single-chain scenario, multi-chains may have unexpected results.

Choose a reason for hiding this comment

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

Whats the current behaviour? I think its probably fine we dont support it but also we should provide an appropriate error if we don't

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Currently, only single-chain projects are supported, and if it is a multi-chain project, an error will be returned.

const metadata = build.input.pgRegistry.pgResources._metadata;

return {
Expand All @@ -55,7 +56,7 @@
const $metadataResult = withPgClientTransaction(
metadata.executor,
$executorContext,
async (client, data) => {

Check warning on line 59 in src/plugins/GetMetadataPlugin.ts

View workflow job for this annotation

GitHub Actions / code-style

'data' is defined but never used
const { rows } = await client.query<MetaEntry>({
text: `select * from "${schemaName}"."_metadata" WHERE key = ANY ($1)`,
values: [METADATA_KEYS],
Expand Down
178 changes: 178 additions & 0 deletions src/plugins/GetSubqueryMetadataPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Copyright 2020-2024 SubQuery Pte Ltd authors & contributors
// SPDX-License-Identifier: GPL-3.0

import { URL } from 'url';

Check warning on line 4 in src/plugins/GetSubqueryMetadataPlugin.ts

View workflow job for this annotation

GitHub Actions / code-style

'URL' is defined but never used
import {
getMetadataTableName,
MetaData,
METADATA_REGEX,
MULTI_METADATA_REGEX,
TableEstimate,
} from '@subql/utils';
import { makeExtendSchemaPlugin, gql, ExtensionDefinition } from 'graphile-utils';
import { PgClient, withPgClientTransaction } from 'postgraphile/@dataplan/pg';
import { constant } from 'postgraphile/grafast';
import { ArgsInterface } from '../config/yargs';

const extensionsTypeDefs: ExtensionDefinition['typeDefs'] = gql`
type TableEstimate {
table: String
estimate: Int
}
type _Metadata {
lastProcessedHeight: Int
lastProcessedTimestamp: Date
targetHeight: Int
chain: String
specName: String
genesisHash: String
startHeight: Int
indexerHealthy: Boolean
indexerNodeVersion: String
queryNodeVersion: String
queryNodeStyle: String
rowCountEstimate: [TableEstimate]
dynamicDatasources: [JSON]
evmChainId: String
deployments: JSON
lastFinalizedVerifiedHeight: Int
unfinalizedBlocks: String
lastCreatedPoiHeight: Int
latestSyncedPoiHeight: Int
dbSize: BigInt
}
type _Metadatas {
totalCount: Int!
nodes: [_Metadata]!
}
extend type Query {
_metadata(chainId: String): _Metadata
_metadatas(after: Cursor, before: Cursor): _Metadatas
}
`;
type MetaType = number | string | boolean;
type MetaEntry = { key: string; value: MetaType };
type MetadatasConnection = {
totalCount?: number;
nodes?: Partial<MetaData>[];
};

const { version: packageVersion } = require('../../package.json');

Check warning on line 60 in src/plugins/GetSubqueryMetadataPlugin.ts

View workflow job for this annotation

GitHub Actions / code-style

Require statement not part of import statement
const META_JSON_FIELDS = ['deployments'];

function matchMetadataTableName(name: string): boolean {
return METADATA_REGEX.test(name) || MULTI_METADATA_REGEX.test(name);
}

async function getTableEstimate(schemaName: string, pgClient: PgClient) {
const { rows } = await pgClient.query<TableEstimate>({
text: `select relname as table , reltuples::bigint as estimate from pg_class
where
relnamespace in (select oid from pg_namespace where nspname = $1)
and
relname in (select table_name from information_schema.tables where table_schema = $1)`,
values: [schemaName],
});
return rows;
}

export function CreateSubqueryMetadataPlugin(schemaName: string, args: ArgsInterface) {

Check warning on line 79 in src/plugins/GetSubqueryMetadataPlugin.ts

View workflow job for this annotation

GitHub Actions / code-style

Missing return type on function

Check warning on line 79 in src/plugins/GetSubqueryMetadataPlugin.ts

View workflow job for this annotation

GitHub Actions / code-style

'args' is defined but never used
return makeExtendSchemaPlugin((build) => {
// Find all metadata table
const pgResources = build.input.pgRegistry.pgResources;
const metadataTables = Object.keys(build.input.pgRegistry.pgResources).filter((tableName) =>
matchMetadataTableName(tableName)
);
const metadataPgResource = metadataTables.reduce(
(result, key) => {
result[key] = pgResources[key];
return result;
},
{} as { [key: string]: (typeof pgResources)[keyof typeof pgResources] }
);

return {
typeDefs: extensionsTypeDefs,

plans: {
Query: {
_metadata($parent, { $chainId }, ...args) {

Check warning on line 99 in src/plugins/GetSubqueryMetadataPlugin.ts

View workflow job for this annotation

GitHub Actions / code-style

'args' is defined but never used
const totalCountInput = $parent.get('totalCount');

Check warning on line 100 in src/plugins/GetSubqueryMetadataPlugin.ts

View workflow job for this annotation

GitHub Actions / code-style

'totalCountInput' is assigned a value but never used
if ($chainId === undefined) {
return;
}

const chainId = $chainId.eval();
const metadataTableName = chainId ? getMetadataTableName(chainId) : '_metadata';
const $metadata = metadataPgResource[metadataTableName];
if (!$metadata) throw new Error(`Not Found Metadata, chainId: ${chainId}`);
const $metadataResult = withPgClientTransaction(
$metadata.executor,
$chainId,
async (pgClient, input): Promise<Partial<MetaData>> => {
const rowCountEstimate = await getTableEstimate(schemaName, pgClient);
const { rows } = await pgClient.query<MetaEntry>({
text: `select value, key from "${schemaName}"."${metadataTableName}"`,
});
const result: Record<string, unknown> = {};
rows.forEach((item) => {
if (META_JSON_FIELDS.includes(item.key)) {
result[item.key] = JSON.parse(item.value as string);
} else {
result[item.key] = item.value;
}
});

result.rowCountEstimate = rowCountEstimate;
result.queryNodeVersion = packageVersion;
result.queryNodeStyle = 'subgraph';
return result;
}
);

return $metadataResult;
},
_metadatas(_, $input) {
const totalCount = Object.keys(metadataPgResource).length;
const pgTable = metadataPgResource[metadataTables[0]];
if (!totalCount || !pgTable) {
return constant({ totalCount: 0, nodes: [] });
}

const $metadataResult = withPgClientTransaction(
pgTable.executor,
$input.getRaw(''),
async (pgClient, input): Promise<MetadatasConnection> => {
const rowCountEstimate = await getTableEstimate(schemaName, pgClient);
const nodes = await Promise.all(
metadataTables.map(async (tableName): Promise<Partial<MetaData>> => {
const { rows } = await pgClient.query({
text: `select value, key from "${schemaName}"."${tableName}"`,
});
const result: Record<string, unknown> = {};
rows.forEach((item: any) => {
if (META_JSON_FIELDS.includes(item.key)) {
result[item.key] = JSON.parse(item.value);
} else {
result[item.key] = item.value;
}
});

result.rowCountEstimate = rowCountEstimate;
result.queryNodeVersion = packageVersion;
result.queryNodeStyle = 'subgraph';
return result;
})
);

return { totalCount, nodes };
}
);

return $metadataResult;
},
},
},
};
});
}
2 changes: 1 addition & 1 deletion src/plugins/filter/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,11 @@ export function ConvertGraphqlType(fieldType: string): GraphQLInputType | undefi
return GraphQLString;
case 'int2':
case 'int4':
case 'int8':
case 'float8':
case 'float4':
case 'numeric':
return GraphQLFloat;
case 'int8':
case 'timestamp':
return GraphQLString;
case 'varchar':
Expand Down
19 changes: 16 additions & 3 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,34 @@
// SPDX-License-Identifier: GPL-3.0

import { createServer } from 'node:http';
import express from 'express';
import { grafserv } from 'grafserv/express/v4';
import { postgraphile } from 'postgraphile';
import { grafserv } from 'postgraphile/grafserv/node';
// import { grafserv } from 'postgraphile/grafserv/node';
import { genPreset, ArgsInterface } from './config/index';

export function startServer(args: ArgsInterface) {
const preset = genPreset(args);
const pgl = postgraphile(preset);
const serv = pgl.createServ(grafserv);

const server = createServer();
const app = express();
app.use((req, res, next) => {
console.log(req.url);
if (req.url === '/.well-known/apollo/server-health') {
res.setHeader('Content-Type', 'application/health+json');
res.end('{"status":"pass"}');
return;
}
next();
});
const server = createServer(app);

server.on('error', (e) => {
console.error(e);
});

serv.addTo(server).catch((e) => {
serv.addTo(app, server).catch((e) => {
console.error(e);
process.exit(1);
});
Expand Down
Loading
Loading