Skip to content

Commit 35ce366

Browse files
Track completed import/exports in a meta repository on Splitgraph
After an import/export has completed, insert a row into the meta table, which we will also use to fetch the previously imported repositories from the client side when rendering the sidebar. We don't have transactional guarantees on the DDN, so we can't do `INSERT ON CONFLICT`, so instead we avoid duplicate rows by first selecting the existing row, and returning `204` if it's already been inserted into the `completed_repositories` table. However, I did notice that when I inserted the same row twice, it only showed up once when I made a selection in the Console. I don't know if this was due to a race condition, a bug, or because it's using the entire row as a compound primary key and for some reason requiring that it be unique.
1 parent f9db15b commit 35ce366

7 files changed

Lines changed: 326 additions & 20 deletions

File tree

examples/nextjs-import-airbyte-github-export-seafowl/.env.test.local

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
SPLITGRAPH_API_KEY="********************************"
88
SPLITGRAPH_API_SECRET="********************************"
99

10+
# This should match the username associated with the API key
11+
SPLITGRAPH_GITHUB_ANALYTICS_META_NAMESPACE="*****"
12+
1013
# Create a GitHub token that can query the repositories you want to connect
1114
# For example, a token with read-only access to public repos is sufficient
1215
# CREATE ONE HERE: https://github.com/settings/personal-access-tokens/new

examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/ExportPanel.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export const ExportPanel = () => {
4545
);
4646

4747
const handleStartExport = useCallback(async () => {
48+
const abortController = new AbortController();
49+
4850
try {
4951
const response = await fetch("/api/start-export-to-seafowl", {
5052
method: "POST",
@@ -55,6 +57,7 @@ export const ExportPanel = () => {
5557
headers: {
5658
"Content-Type": "application/json",
5759
},
60+
signal: abortController.signal,
5861
});
5962
const data = (await response.json()) as StartExportToSeafowlResponseData;
6063

@@ -87,8 +90,14 @@ export const ExportPanel = () => {
8790
],
8891
});
8992
} catch (error) {
93+
if (error.name === "AbortError") {
94+
return;
95+
}
96+
9097
dispatch({ type: "export_error", error: error.message });
9198
}
99+
100+
return () => abortController.abort();
92101
}, [queriesToExport, tablesToExport, dispatch]);
93102

94103
return (

examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/StepperContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// StepperContext.tsx
2-
import React, { useReducer, useContext, ReactNode } from "react";
2+
import React, { useContext, ReactNode } from "react";
33
import {
44
StepperState,
55
StepperAction,

examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/stepper-states.ts

Lines changed: 82 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,6 @@ export type StepperAction =
4545
| { type: "reset" }
4646
| { type: "initialize_from_url"; parsedFromUrl: StepperState };
4747

48-
type ExtractStepperAction<T extends StepperAction["type"]> = Extract<
49-
StepperAction,
50-
{ type: T }
51-
>;
52-
5348
const initialState: StepperState = {
5449
stepperState: "unstarted",
5550
repository: null,
@@ -62,19 +57,6 @@ const initialState: StepperState = {
6257
exportError: null,
6358
};
6459

65-
// FOR DEBUGGING: uncomment for hardcoded state initialization
66-
// export const initialState: StepperState = {
67-
// ...normalInitialState,
68-
// stepperState: "import_complete",
69-
// splitgraphNamespace: "miles",
70-
// splitgraphRepository: "import-via-nextjs",
71-
// };
72-
73-
type ActionParams<T extends StepperAction["type"]> = Omit<
74-
ExtractStepperAction<T>,
75-
"type"
76-
>;
77-
7860
const getQueryParamAsString = <T extends string = string>(
7961
query: ParsedUrlQuery,
8062
key: string
@@ -310,13 +292,95 @@ const urlNeedsChange = (state: StepperState, router: NextRouter) => {
310292
);
311293
};
312294

295+
/**
296+
* When the export has completed, send a request to /api/mark-import-export-complete
297+
* which will insert the repository into the metadata table, which we query to
298+
* render the sidebar
299+
*/
300+
const useMarkAsComplete = (
301+
state: StepperState,
302+
dispatch: React.Dispatch<StepperAction>
303+
) => {
304+
useEffect(() => {
305+
if (state.stepperState !== "export_complete") {
306+
return;
307+
}
308+
309+
const {
310+
repository: {
311+
namespace: githubSourceNamespace,
312+
repository: githubSourceRepository,
313+
},
314+
splitgraphRepository: splitgraphDestinationRepository,
315+
} = state;
316+
317+
// NOTE: Make sure to abort request so that in React 18 development mode,
318+
// when effect runs twice, the second request is aborted and we don't have
319+
// a race condition with two requests inserting into the table (where we have no transactional
320+
// integrity and manually do a SELECT before the INSERT to check if the row already exists)
321+
const abortController = new AbortController();
322+
323+
const markImportExportComplete = async () => {
324+
try {
325+
const response = await fetch("/api/mark-import-export-complete", {
326+
method: "POST",
327+
headers: {
328+
"Content-Type": "application/json",
329+
},
330+
body: JSON.stringify({
331+
githubSourceNamespace,
332+
githubSourceRepository,
333+
splitgraphDestinationRepository,
334+
}),
335+
signal: abortController.signal,
336+
});
337+
338+
if (!response.ok) {
339+
throw new Error("Failed to mark import/export as complete");
340+
}
341+
342+
const data = await response.json();
343+
344+
if (!data.status) {
345+
throw new Error(
346+
"Got unexpected resposne shape when marking import/export complete"
347+
);
348+
}
349+
350+
if (data.error) {
351+
throw new Error(
352+
`Failed to mark import/export complete: ${data.error}`
353+
);
354+
}
355+
356+
console.log("Marked import/export as complete");
357+
} catch (error) {
358+
if (error.name === "AbortError") {
359+
return;
360+
}
361+
362+
dispatch({
363+
type: "export_error",
364+
error: error.message ?? error.toString(),
365+
});
366+
}
367+
};
368+
369+
markImportExportComplete();
370+
371+
return () => abortController.abort();
372+
}, [state, dispatch]);
373+
};
374+
313375
export const useStepperReducer = () => {
314376
const router = useRouter();
315377
const [state, dispatch] = useReducer(stepperReducer, {
316378
...initialState,
317379
stepperState: "uninitialized",
318380
});
319381

382+
useMarkAsComplete(state, dispatch);
383+
320384
useEffect(() => {
321385
dispatch({
322386
type: "initialize_from_url",

examples/nextjs-import-airbyte-github-export-seafowl/env-vars.d.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,58 @@ namespace NodeJS {
3737
* This is useful for debugging and development.
3838
*/
3939
MITMPROXY_ADDRESS?: string;
40+
41+
/**
42+
* The namespace of the repository in Splitgraph where metadata is stored
43+
* containing the state of imported GitHub repositories, which should contain
44+
* the repository `SPLITGRAPH_GITHUB_ANALYTICS_META_REPOSITORY`.
45+
*
46+
* This should be defined in `.env.local`, since it's not checked into Git
47+
* and can vary between users. It should match the username associated with
48+
* the `SPLITGRAPH_API_KEY`
49+
*
50+
* Example:
51+
*
52+
* ```
53+
* miles/splitgraph-github-analytics.completed_repositories
54+
* ^^^^^
55+
* SPLITGRAPH_GITHUB_ANALYTICS_META_NAMESPACE=miles
56+
* ```
57+
*/
58+
SPLITGRAPH_GITHUB_ANALYTICS_META_NAMESPACE: string;
59+
60+
/**
61+
* The repository (no namespace) in Splitgraph where metadata is stored
62+
* containing the state of imported GitHub repositories, which should be a
63+
* repository contained inside `SPLITGRAPH_GITHUB_ANALYTICS_META_NAMESPACE`.
64+
*
65+
* This is defined by default in `.env` which is checked into Git.
66+
*
67+
* * Example:
68+
*
69+
* ```
70+
* miles/splitgraph-github-analytics.completed_repositories
71+
* ^^^^^^^^^^^^^^^^^^^^^^^^^^^
72+
* SPLITGRAPH_GITHUB_ANALYTICS_META_REPOSITORY=splitgraph-github-analytics
73+
* ```
74+
*/
75+
SPLITGRAPH_GITHUB_ANALYTICS_META_REPOSITORY: string;
76+
77+
/**
78+
* The name of the table containing completed repositories, which are inserted
79+
* when the import/export is complete, and which can be queried to render the
80+
* sidebar containing previously imported github repositories.
81+
*
82+
* This is defined by default in `.env` which is checked into Git.
83+
*
84+
* Example:
85+
*
86+
* ```
87+
* miles/splitgraph-github-analytics.completed_repositories
88+
* ^^^^^^^^^^^^^^^^^^^^^^
89+
* SPLITGRAPH_GITHUB_ANALYTICS_META_COMPLETED_TABLE=completed_repositories
90+
* ```
91+
*/
92+
SPLITGRAPH_GITHUB_ANALYTICS_META_COMPLETED_TABLE: string;
4093
}
4194
}

examples/nextjs-import-airbyte-github-export-seafowl/lib/backend/splitgraph-db.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { makeSplitgraphDb } from "@madatdata/core";
1+
import { makeSplitgraphDb, makeSplitgraphHTTPContext } from "@madatdata/core";
22

33
// TODO: fix plugin exports
44
import { makeDefaultPluginList } from "@madatdata/db-splitgraph";
@@ -33,6 +33,15 @@ export const makeAuthenticatedSplitgraphDb = () =>
3333
}),
3434
});
3535

36+
export const makeAuthenticatedSplitgraphHTTPContext = () =>
37+
makeSplitgraphHTTPContext({
38+
authenticatedCredential,
39+
plugins: makeDefaultPluginList({
40+
graphqlEndpoint: defaultSplitgraphHost.baseUrls.gql,
41+
authenticatedCredential,
42+
}),
43+
});
44+
3645
// TODO: export this utility function from the library
3746
export const claimsFromJWT = (jwt?: string) => {
3847
if (!jwt) {

0 commit comments

Comments
 (0)