Skip to content

Commit 94989da

Browse files
authored
feat: track hasura tables directly from your dapp (#1161)
## Summary by Sourcery Provide first-class support for tracking Hasura tables directly from the SDK CLI by introducing a reusable metadata client and tracking utility, update codegen and bundler configs to include the new client, and expand test coverage with relevant E2E scenarios. New Features: - Introduce createHasuraMetadataClient and trackAllTables utilities for interacting with Hasura metadata API - Enable direct tracking of all tables in Hasura via the CLI using the new metadata client - Add end-to-end tests for Hasura table tracking and basic GraphQL querying Bug Fixes: - Correct pause/resume blockchain node tests by removing redundant cleanup steps and adjusting expect usage Enhancements: - Refactor CLI track command to delegate metadata operations to the shared trackAllTables utility - Extend codegen templates to instantiate and expose the Hasura metadata client - Enhance tsdown bundler configuration to support optional .d.ts output and include new externals Build: - Add @settlemint/sdk-hasura as a dependency and include it in CLI tsdown externals Tests: - Add hasura.e2e.test.ts to verify trackAllTables and GraphQL client functionality - Simplify existing pause/resume tests by eliminating unnecessary waiting commands
1 parent 1923abd commit 94989da

12 files changed

Lines changed: 3920 additions & 103 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ test/unknown
4747
test/contracts
4848
test/contracts-subgraphs
4949
test/contracts-subgraphs-not-generated
50-
test/test-app
50+
test/test-app/*
5151
!test/test-app/portal-env.d.ts
52+
!test/test-app/hasura-env.d.ts
5253
.temp-mono-repo-test
5354

5455
# Docs

bun.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"@inquirer/input": "4.2.0",
5353
"@inquirer/password": "4.0.16",
5454
"@inquirer/select": "4.2.4",
55+
"@settlemint/sdk-hasura": "workspace:*",
5556
"@settlemint/sdk-js": "workspace:*",
5657
"@settlemint/sdk-utils": "workspace:*",
5758
"@settlemint/sdk-viem": "workspace:*",

sdk/cli/src/commands/codegen/codegen-hasura.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { writeTemplate } from "@/commands/codegen/utils/write-template";
2-
import { getApplicationOrPersonalAccessToken } from "@/utils/get-app-or-personal-token";
31
import { generateSchema } from "@gql.tada/cli-utils";
42
import { projectRoot } from "@settlemint/sdk-utils/filesystem";
53
import { installDependencies, isPackageInstalled } from "@settlemint/sdk-utils/package-manager";
64
import { note } from "@settlemint/sdk-utils/terminal";
75
import { type DotEnv, LOCAL_INSTANCE, STANDALONE_INSTANCE } from "@settlemint/sdk-utils/validation";
6+
import { writeTemplate } from "@/commands/codegen/utils/write-template";
7+
import { getApplicationOrPersonalAccessToken } from "@/utils/get-app-or-personal-token";
88

99
const PACKAGE_NAME = "@settlemint/sdk-hasura";
1010

@@ -62,7 +62,13 @@ export const { client: hasuraClient, graphql: hasuraGraphql } = createHasuraClie
6262
adminSecret: process.env.SETTLEMINT_HASURA_ADMIN_SECRET!,
6363
}, {
6464
fetch: requestLogger(logger, "hasura", fetch) as typeof fetch,
65-
});`;
65+
});
66+
67+
export const hasuraMetadataClient = createHasuraMetadataClient({
68+
instance: process.env.SETTLEMINT_HASURA_ENDPOINT!,
69+
accessToken: process.env.SETTLEMINT_ACCESS_TOKEN,
70+
adminSecret: process.env.SETTLEMINT_HASURA_ADMIN_SECRET!,
71+
}, logger);`;
6672

6773
await writeTemplate(hasuraTemplate, "/lib/settlemint", "hasura.ts");
6874
} else {

sdk/cli/src/commands/codegen/codegen-portal.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { writeTemplate } from "@/commands/codegen/utils/write-template";
2-
import { getApplicationOrPersonalAccessToken } from "@/utils/get-app-or-personal-token";
31
import { generateSchema } from "@gql.tada/cli-utils";
42
import { projectRoot } from "@settlemint/sdk-utils/filesystem";
53
import { installDependencies, isPackageInstalled } from "@settlemint/sdk-utils/package-manager";
64
import { type DotEnv, LOCAL_INSTANCE, STANDALONE_INSTANCE } from "@settlemint/sdk-utils/validation";
5+
import { writeTemplate } from "@/commands/codegen/utils/write-template";
6+
import { getApplicationOrPersonalAccessToken } from "@/utils/get-app-or-personal-token";
77

88
const PACKAGE_NAME = "@settlemint/sdk-portal";
99
export async function codegenPortal(env: DotEnv) {
@@ -53,7 +53,7 @@ export const { client: portalClient, graphql: portalGraphql } = createPortalClie
5353
fetch: requestLogger(logger, "portal", fetch) as typeof fetch,
5454
});
5555
56-
export const getPortalWebsocketClient = getWebsocketClient({
56+
export const portalWebsocketClient = getWebsocketClient({
5757
portalGraphqlEndpoint: process.env.SETTLEMINT_PORTAL_GRAPHQL_ENDPOINT!,
5858
accessToken: process.env.SETTLEMINT_ACCESS_TOKEN,
5959
});

sdk/cli/src/commands/hasura/track.ts

Lines changed: 11 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import { Command } from "@commander-js/extra-typings";
2+
import { createHasuraMetadataClient, trackAllTables } from "@settlemint/sdk-hasura";
3+
import { createSettleMintClient } from "@settlemint/sdk-js";
4+
import { loadEnv } from "@settlemint/sdk-utils/environment";
5+
import { intro, note, outro } from "@settlemint/sdk-utils/terminal";
6+
import { type DotEnv, LOCAL_INSTANCE, STANDALONE_INSTANCE } from "@settlemint/sdk-utils/validation";
17
import { missingApplication } from "@/error/missing-config-error";
28
import { nothingSelectedError } from "@/error/nothing-selected-error";
39
import { hasuraPrompt } from "@/prompts/cluster-service/hasura.prompt";
@@ -6,13 +12,6 @@ import { serviceSpinner } from "@/spinners/service.spinner";
612
import { createExamples } from "@/utils/commands/create-examples";
713
import { getApplicationOrPersonalAccessToken } from "@/utils/get-app-or-personal-token";
814
import { getHasuraEnv } from "@/utils/get-cluster-service-env";
9-
import { Command } from "@commander-js/extra-typings";
10-
import { createSettleMintClient } from "@settlemint/sdk-js";
11-
import { extractBaseUrlBeforeSegment } from "@settlemint/sdk-utils";
12-
import { loadEnv } from "@settlemint/sdk-utils/environment";
13-
import { appendHeaders } from "@settlemint/sdk-utils/http";
14-
import { intro, note, outro, spinner } from "@settlemint/sdk-utils/terminal";
15-
import { type DotEnv, LOCAL_INSTANCE, STANDALONE_INSTANCE } from "@settlemint/sdk-utils/validation";
1615

1716
export function hasuraTrackCommand() {
1817
return new Command("track")
@@ -89,94 +88,12 @@ export function hasuraTrackCommand() {
8988
return note("Could not retrieve Hasura endpoint or admin secret. Please check your configuration.");
9089
}
9190

92-
// Convert GraphQL endpoint to Query endpoint
93-
const baseUrl = extractBaseUrlBeforeSegment(hasuraGraphqlEndpoint, "/v1/graphql");
94-
const queryEndpoint = new URL(`${baseUrl}/v1/metadata`).toString();
95-
96-
const messages: string[] = [];
97-
98-
const { result } = await spinner({
99-
startMessage: `Tracking all tables in Hasura from database "${database}"`,
100-
stopMessage: "Successfully tracked all tables in Hasura",
101-
task: async () => {
102-
const executeHasuraQuery = async <T>(query: object): Promise<{ ok: boolean; data: T }> => {
103-
const response = await fetch(queryEndpoint, {
104-
method: "POST",
105-
headers: appendHeaders(
106-
{
107-
"Content-Type": "application/json",
108-
"X-Hasura-Admin-Secret": hasuraAdminSecret,
109-
},
110-
{
111-
"x-auth-token": accessToken,
112-
},
113-
),
114-
body: JSON.stringify(query),
115-
});
116-
117-
if (!response.ok) {
118-
return { ok: false, data: (await response.json()) as T };
119-
}
120-
121-
return { ok: true, data: (await response.json()) as T };
122-
};
123-
124-
// Get all tables using pg_get_source_tables
125-
const getTablesResult = await executeHasuraQuery<
126-
Array<{
127-
name: string;
128-
schema: string;
129-
}>
130-
>({
131-
type: "pg_get_source_tables",
132-
args: {
133-
source: database,
134-
},
135-
});
136-
137-
if (!getTablesResult.ok) {
138-
throw new Error(`Failed to get tables: ${JSON.stringify(getTablesResult.data)}`);
139-
}
140-
141-
const tables = getTablesResult.data;
142-
143-
if (tables.length === 0) {
144-
return { result: "no-tables" as const };
145-
}
146-
147-
messages.push(`Found ${tables.length} tables in database "${database}"`);
148-
149-
// Incase a table is already tracked, untrack it first
150-
await executeHasuraQuery<{ code?: string }>({
151-
type: "pg_untrack_tables",
152-
args: {
153-
tables: tables.map((table) => ({
154-
table: table.name,
155-
})),
156-
allow_warnings: true,
157-
},
158-
});
159-
160-
// Track all tables
161-
const trackResult = await executeHasuraQuery<{ code?: string }>({
162-
type: "pg_track_tables",
163-
args: {
164-
tables: tables.map((table) => ({
165-
table: table.name,
166-
})),
167-
allow_warnings: true,
168-
},
169-
});
170-
171-
if (!trackResult.ok) {
172-
throw new Error(`Failed to track tables: ${JSON.stringify(trackResult.data)}`);
173-
}
174-
175-
messages.push(`Successfully tracked ${tables.length} tables`);
176-
177-
return { result: "success" as const };
178-
},
91+
const hasuraMetadataClient = createHasuraMetadataClient({
92+
instance: hasuraGraphqlEndpoint,
93+
accessToken,
94+
adminSecret: hasuraAdminSecret,
17995
});
96+
const { result, messages } = await trackAllTables(database, hasuraMetadataClient);
18097

18198
// Display collected messages after spinner completes
18299
for (const message of messages) {

sdk/cli/tsdown.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export default defineConfig(
77
createCLIPackage(["src/cli.ts"], {
88
external: [
99
"node:*",
10+
"@settlemint/sdk-hasura",
1011
"@settlemint/sdk-js",
1112
"@settlemint/sdk-utils",
1213
"node-fetch-native",
@@ -17,6 +18,7 @@ export default defineConfig(
1718
"@inquirer/input",
1819
"@inquirer/password",
1920
"@inquirer/select",
21+
"@gql.tada/cli-utils",
2022
],
2123
define: {
2224
__CLI_NAME__: '"settlemint"',

sdk/hasura/src/hasura.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { extractBaseUrlBeforeSegment } from "@settlemint/sdk-utils";
12
import { appendHeaders } from "@settlemint/sdk-utils/http";
23
import { type Logger, requestLogger } from "@settlemint/sdk-utils/logging";
34
import { ensureServer } from "@settlemint/sdk-utils/runtime";
@@ -105,5 +106,57 @@ export function createHasuraClient<const Setup extends AbstractSetupSchema>(
105106
};
106107
}
107108

108-
export { readFragment } from "gql.tada";
109+
/**
110+
* Creates a Hasura Metadata client
111+
*
112+
* @param options - Configuration options for the client
113+
* @param logger - Optional logger to use for logging the requests
114+
* @returns A function that can be used to make requests to the Hasura Metadata API
115+
* @throws Will throw an error if the options fail validation against ClientOptionsSchema
116+
* @example
117+
* import { createHasuraMetadataClient } from '@settlemint/sdk-hasura';
118+
*
119+
* const client = createHasuraMetadataClient({
120+
* instance: process.env.SETTLEMINT_HASURA_ENDPOINT,
121+
* accessToken: process.env.SETTLEMINT_ACCESS_TOKEN,
122+
* adminSecret: process.env.SETTLEMINT_HASURA_ADMIN_SECRET,
123+
* });
124+
*
125+
* const result = await client({
126+
* type: "pg_get_source_tables",
127+
* args: {
128+
* source: "default",
129+
* },
130+
* });
131+
*/
132+
export function createHasuraMetadataClient(options: ClientOptions, logger?: Logger) {
133+
ensureServer();
134+
const validatedOptions = validate(ClientOptionsSchema, options);
135+
const baseUrl = extractBaseUrlBeforeSegment(options.instance, "/v1/graphql");
136+
const queryEndpoint = new URL(`${baseUrl}/v1/metadata`).toString();
137+
const fetchInstance = logger ? requestLogger(logger, "hasura", fetch) : fetch;
138+
139+
return async <T>(query: object): Promise<{ ok: boolean; data: T }> => {
140+
const response = await fetchInstance(queryEndpoint, {
141+
method: "POST",
142+
headers: appendHeaders(
143+
{ "Content-Type": "application/json" },
144+
{
145+
"x-auth-token": validatedOptions.accessToken,
146+
"x-hasura-admin-secret": validatedOptions.adminSecret,
147+
},
148+
),
149+
body: JSON.stringify(query),
150+
});
151+
152+
if (!response.ok) {
153+
return { ok: false, data: (await response.json()) as T };
154+
}
155+
156+
return { ok: true, data: (await response.json()) as T };
157+
};
158+
}
159+
109160
export type { FragmentOf, ResultOf, VariablesOf } from "gql.tada";
161+
export { readFragment } from "gql.tada";
162+
export { trackAllTables } from "./utils/track-all-tables.js";
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import type { createHasuraMetadataClient } from "../hasura.js";
2+
3+
/**
4+
* Track all tables in a database
5+
*
6+
* @param databaseName - The name of the database to track tables for
7+
* @param options - The client options to use for the Hasura client
8+
* @returns A promise that resolves to an object with a result property indicating success or failure
9+
* @example
10+
* import { trackAllTables } from "@settlemint/sdk-hasura/utils/track-all-tables";
11+
*
12+
* const client = createHasuraMetadataClient({
13+
* instance: "http://localhost:8080",
14+
* accessToken: "test",
15+
* adminSecret: "test",
16+
* });
17+
*
18+
* const result = await trackAllTables("default", client);
19+
* if (result.result === "success") {
20+
* console.log("Tables tracked successfully");
21+
* } else {
22+
* console.error("Failed to track tables");
23+
* }
24+
*/
25+
export async function trackAllTables(
26+
databaseName: string,
27+
client: ReturnType<typeof createHasuraMetadataClient>,
28+
): Promise<{ result: "success" | "no-tables"; messages: string[] }> {
29+
const messages: string[] = [];
30+
31+
// Get all tables using pg_get_source_tables
32+
const getTablesResult = await client<
33+
Array<{
34+
name: string;
35+
schema: string;
36+
}>
37+
>({
38+
type: "pg_get_source_tables",
39+
args: {
40+
source: databaseName,
41+
},
42+
});
43+
44+
if (!getTablesResult.ok) {
45+
throw new Error(`Failed to get tables: ${JSON.stringify(getTablesResult.data)}`);
46+
}
47+
48+
const tables = getTablesResult.data;
49+
50+
if (tables.length === 0) {
51+
return { result: "no-tables" as const, messages };
52+
}
53+
54+
messages.push(`Found ${tables.length} tables in database "${databaseName}"`);
55+
56+
// Incase a table is already tracked, untrack it first
57+
await client<{ code?: string }>({
58+
type: "pg_untrack_tables",
59+
args: {
60+
tables: tables.map((table) => ({
61+
table: table.name,
62+
})),
63+
allow_warnings: true,
64+
},
65+
});
66+
67+
// Track all tables
68+
const trackResult = await client<{ code?: string }>({
69+
type: "pg_track_tables",
70+
args: {
71+
tables: tables.map((table) => ({
72+
table: table.name,
73+
})),
74+
allow_warnings: true,
75+
},
76+
});
77+
78+
if (!trackResult.ok) {
79+
throw new Error(`Failed to track tables: ${JSON.stringify(trackResult.data)}`);
80+
}
81+
82+
messages.push(`Successfully tracked ${tables.length} tables`);
83+
84+
return { result: "success" as const, messages };
85+
}

shared/tsdown-factory.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface ConfigOptions {
1111
banner?: { js?: string };
1212
define?: Record<string, string>;
1313
minifyOverride?: boolean;
14+
dts?: boolean;
1415
}
1516

1617
const isProd = process.env.NODE_ENV === "production";
@@ -19,7 +20,7 @@ export const createConfig = (options: ConfigOptions): Options => {
1920
const config: Options = {
2021
entry: options.entry,
2122
format: options.format || ["cjs", "esm"],
22-
dts: true,
23+
dts: options.dts ?? true,
2324
sourcemap: !isProd || "inline",
2425
treeshake: isProd,
2526
minify: options.minifyOverride ?? isProd,
@@ -83,6 +84,7 @@ export const createCLIPackage = (entry: string[], options: Partial<ConfigOptions
8384
define: {
8485
__CLI_VERSION__: JSON.stringify(process.env.npm_package_version || "dev"),
8586
},
87+
dts: false,
8688
...options,
8789
});
8890

0 commit comments

Comments
 (0)