Skip to content

Commit 9c596ef

Browse files
Implement backend API routes start-export-to-seafowl and await-export-to-seafowl-task
The `start-export-to-seafowl` route takes a list of source tables from Splitgraph (list of `{namespace,repository,table}`), and starts a task to export them to Seafowl. It returns a list of objects `{taskId: string; tableName: string;}`, where each item represents the currently exporting table (and `tableName` is the source table name). The `await-export-to-seafowl-task` route takes a single `taskId` parameter and returns its status, i.e. `{completed: boolean; ...otherInfo}`
1 parent a1df06e commit 9c596ef

2 files changed

Lines changed: 179 additions & 0 deletions

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import type { NextApiRequest, NextApiResponse } from "next";
2+
import { makeAuthenticatedSplitgraphDb } from "../../lib-backend/splitgraph-db";
3+
import type { DeferredSplitgraphExportTask } from "@madatdata/db-splitgraph/plugins/exporters/splitgraph-base-export-plugin";
4+
5+
type ResponseData =
6+
| {
7+
completed: boolean;
8+
jobStatus: DeferredSplitgraphExportTask["response"];
9+
}
10+
| { error: string; completed: false };
11+
12+
/**
13+
* To manually send a request, example:
14+
15+
```bash
16+
curl -i \
17+
-H "Content-Type: application/json" http://localhost:3000/api/await-export-to-seafowl-task \
18+
-d '{ "taskId": "2923fd6f-2197-495a-9df1-2428a9ca8dee" }'
19+
```
20+
*/
21+
export default async function handler(
22+
req: NextApiRequest,
23+
res: NextApiResponse<ResponseData>
24+
) {
25+
if (!req.body["taskId"]) {
26+
res.status(400).json({
27+
error: "Missing required key: taskId",
28+
completed: false,
29+
});
30+
return;
31+
}
32+
33+
const { taskId } = req.body;
34+
35+
try {
36+
const maybeCompletedTask = await pollImport({
37+
splitgraphTaskId: taskId,
38+
});
39+
40+
if (maybeCompletedTask.error) {
41+
throw new Error(JSON.stringify(maybeCompletedTask.error));
42+
}
43+
44+
res.status(200).json(maybeCompletedTask);
45+
return;
46+
} catch (err) {
47+
res.status(400).json({
48+
error: err.message,
49+
completed: false,
50+
});
51+
return;
52+
}
53+
}
54+
55+
const pollImport = async ({
56+
splitgraphTaskId,
57+
}: {
58+
splitgraphTaskId: string;
59+
}) => {
60+
const db = makeAuthenticatedSplitgraphDb();
61+
62+
// NOTE: We must call this, or else requests will fail silently
63+
await db.fetchAccessToken();
64+
65+
const maybeCompletedTask = (await db.pollDeferredTask("export-to-seafowl", {
66+
taskId: splitgraphTaskId,
67+
})) as DeferredSplitgraphExportTask;
68+
69+
// NOTE: We do not include the jobLog, in case it could leak the GitHub PAT
70+
// (remember we're using our PAT on behalf of the users of this app)
71+
return {
72+
completed: maybeCompletedTask?.completed ?? false,
73+
jobStatus: maybeCompletedTask?.response,
74+
error: maybeCompletedTask?.error ?? undefined,
75+
};
76+
};
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import type { NextApiRequest, NextApiResponse } from "next";
2+
import { makeAuthenticatedSplitgraphDb } from "../../lib-backend/splitgraph-db";
3+
4+
type ResponseData =
5+
| {
6+
tables: {
7+
tableName: string;
8+
taskId: string;
9+
}[];
10+
}
11+
| { error: string };
12+
13+
type TableInput = { namespace: string; repository: string; table: string };
14+
15+
/**
16+
* To manually send a request, example:
17+
18+
```bash
19+
curl -i \
20+
-H "Content-Type: application/json" http://localhost:3000/api/start-export-to-seafowl \
21+
-d '{ "tables": [{"namespace": "miles", "repository": "import-via-nextjs", "table": "stargazers"}] }'
22+
```
23+
*/
24+
export default async function handler(
25+
req: NextApiRequest,
26+
res: NextApiResponse<ResponseData>
27+
) {
28+
const db = makeAuthenticatedSplitgraphDb();
29+
const { tables } = req.body;
30+
31+
if (
32+
!tables ||
33+
!tables.length ||
34+
!tables.every(
35+
(t: TableInput) =>
36+
t.namespace &&
37+
t.repository &&
38+
t.table &&
39+
typeof t.namespace === "string" &&
40+
typeof t.repository === "string" &&
41+
typeof t.table === "string"
42+
)
43+
) {
44+
res.status(400).json({ error: "invalid tables input in request body" });
45+
return;
46+
}
47+
48+
try {
49+
const exportingTables = await startExport({
50+
db,
51+
tables,
52+
});
53+
res.status(200).json({
54+
tables: exportingTables,
55+
});
56+
} catch (err) {
57+
res.status(400).json({
58+
error: err.message,
59+
});
60+
}
61+
}
62+
63+
const startExport = async ({
64+
db,
65+
tables,
66+
}: {
67+
db: ReturnType<typeof makeAuthenticatedSplitgraphDb>;
68+
tables: TableInput[];
69+
}) => {
70+
await db.fetchAccessToken();
71+
72+
const response = await db.exportData(
73+
"export-to-seafowl",
74+
{
75+
tables: tables.map((splitgraphSource) => ({
76+
source: {
77+
repository: splitgraphSource.repository,
78+
namespace: splitgraphSource.namespace,
79+
table: splitgraphSource.table,
80+
},
81+
})),
82+
},
83+
{
84+
// Empty instance will trigger Splitgraph to export to demo.seafowl.cloud
85+
seafowlInstance: {},
86+
},
87+
{ defer: true }
88+
);
89+
90+
if (response.error) {
91+
throw new Error(JSON.stringify(response.error));
92+
}
93+
94+
const loadingTables: { taskId: string; tableName: string }[] =
95+
response.taskIds.tables.map(
96+
(t: { jobId: string; sourceTable: string }) => ({
97+
taskId: t.jobId,
98+
tableName: t.sourceTable,
99+
})
100+
);
101+
102+
return loadingTables;
103+
};

0 commit comments

Comments
 (0)