Skip to content

Commit 691ef0d

Browse files
authored
ENG-490 NextJS endpoints to access the sync functions (#230)
1 parent 4311937 commit 691ef0d

3 files changed

Lines changed: 146 additions & 3 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { NextResponse, NextRequest } from "next/server";
2+
import { PostgrestSingleResponse } from "@supabase/supabase-js";
3+
import { Database, Constants } from "@repo/database/types.gen.ts";
4+
import { createClient } from "~/utils/supabase/server";
5+
import {
6+
createApiResponse,
7+
asPostgrestFailure,
8+
handleRouteError,
9+
} from "~/utils/supabase/apiUtils";
10+
11+
type ApiParams = Promise<{ target: string; fn: string; worker: string }>;
12+
export type SegmentDataType = { params: ApiParams };
13+
14+
// POST the task status to the /supabase/sync-task/{function_name}/{target}/{worker} endpoint
15+
export const POST = async (
16+
request: NextRequest,
17+
segmentData: SegmentDataType,
18+
): Promise<NextResponse> => {
19+
try {
20+
const { target, fn, worker } = await segmentData.params;
21+
const targetN = Number.parseInt(target);
22+
if (isNaN(targetN)) {
23+
return createApiResponse(
24+
request,
25+
asPostgrestFailure(`${target} is not a number`, "type"),
26+
);
27+
}
28+
const infoS: string = await request.json();
29+
if (
30+
!(Constants.public.Enums.task_status as readonly string[]).includes(infoS)
31+
) {
32+
return createApiResponse(
33+
request,
34+
asPostgrestFailure(`${infoS} is not a task status`, "type"),
35+
);
36+
}
37+
const info = infoS as Database["public"]["Enums"]["task_status"];
38+
const supabase = await createClient();
39+
const response = (await supabase.rpc("end_sync_task", {
40+
s_target: targetN,
41+
s_function: fn,
42+
s_worker: worker,
43+
s_status: info,
44+
})) as PostgrestSingleResponse<boolean>;
45+
// Transform 204 No Content to 200 OK with success indicator for API consistency
46+
if (response.status === 204) {
47+
response.data = true;
48+
response.status = 200;
49+
}
50+
51+
return createApiResponse(request, response);
52+
} catch (e: unknown) {
53+
return handleRouteError(request, e, "/api/supabase/sync-task");
54+
}
55+
};
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { NextResponse, NextRequest } from "next/server";
2+
import type { PostgrestSingleResponse } from "@supabase/supabase-js";
3+
import { createClient } from "~/utils/supabase/server";
4+
import {
5+
createApiResponse,
6+
handleRouteError,
7+
defaultOptionsHandler,
8+
asPostgrestFailure,
9+
} from "~/utils/supabase/apiUtils";
10+
11+
type SyncTaskInfo = {
12+
worker: string;
13+
timeout?: string;
14+
task_interval?: string;
15+
};
16+
17+
const SYNC_DEFAULTS: Partial<SyncTaskInfo> = {
18+
timeout: "20s",
19+
task_interval: "45s",
20+
};
21+
22+
type ApiParams = Promise<{ target: string; fn: string }>;
23+
export type SegmentDataType = { params: ApiParams };
24+
25+
// POST with the SyncTaskInfo to the /supabase/sync-task/{function_name}/{target} endpoint
26+
export const POST = async (
27+
request: NextRequest,
28+
segmentData: SegmentDataType,
29+
): Promise<NextResponse> => {
30+
try {
31+
const { target, fn } = await segmentData.params;
32+
const targetN = Number.parseInt(target);
33+
if (isNaN(targetN)) {
34+
return createApiResponse(
35+
request,
36+
asPostgrestFailure(`${target} is not a number`, "type"),
37+
);
38+
}
39+
const info: SyncTaskInfo = { ...SYNC_DEFAULTS, ...(await request.json()) };
40+
if (!info.worker) {
41+
return createApiResponse(
42+
request,
43+
asPostgrestFailure("Worker field is required", "invalid"),
44+
);
45+
}
46+
const supabase = await createClient();
47+
const response = (await supabase.rpc("propose_sync_task", {
48+
s_target: targetN,
49+
s_function: fn,
50+
s_worker: info.worker,
51+
timeout: info.timeout,
52+
task_interval: info.task_interval,
53+
})) as PostgrestSingleResponse<Date | null | boolean>;
54+
if (response.data === null) {
55+
// NextJS responses cannot handle null values, convert to boolean success indicator
56+
response.data = true;
57+
}
58+
59+
return createApiResponse(request, response);
60+
} catch (e: unknown) {
61+
return handleRouteError(request, e, "/api/supabase/route");
62+
}
63+
};
64+
65+
// GET the sync_info table from /supabase/sync-task/{function_name}/{target} (should not be necessary)
66+
export const GET = async (
67+
request: NextRequest,
68+
segmentData: SegmentDataType,
69+
): Promise<NextResponse> => {
70+
const { target, fn } = await segmentData.params;
71+
const targetN = Number.parseInt(target);
72+
if (isNaN(targetN)) {
73+
return createApiResponse(
74+
request,
75+
asPostgrestFailure(`${targetN} is not a number`, "type"),
76+
);
77+
}
78+
const supabase = await createClient();
79+
const response = await supabase
80+
.from("sync_info")
81+
.select()
82+
.eq("sync_target", targetN)
83+
.eq("sync_function", fn)
84+
.maybeSingle();
85+
return createApiResponse(request, response);
86+
};
87+
88+
export const OPTIONS = defaultOptionsHandler;

packages/database/doc/sync_functions.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# Sync information
22

3-
The `sync_info` table is meant to always be accessed through one of these two functions: `propose_sync_task` and `end_sync_task`.
3+
The `sync_info` table is meant to always be accessed through one of these two functions: `propose_sync_task` and `end_sync_task`, used through POSTS to the web api endpoints `api/supabase/sync-task/[fn]/[target]` and `api/supabase/sync-task/[fn]/[target]/[worker]` respectively.
44
This acts as a semaphore, so that two workers (e.g. the roam plugin on two different browsers) do not try to run the same sync task at the same time. So you need to give the function `propose_sync_task` enough information to distinguish what you mean to do:
55

66
1. The `target`, e.g. the database Id of the scope of the task, usually a space, but it could be a single content or concept (for reactive updates)
7-
2. a `function` name, to distinguish different tasks on the same target; e.g. adding vs deleting content. (arbitrary short string)
8-
3. the `worker` name: random string, should be the same between calls.
7+
2. a `function` name, to distinguish different tasks on the same target; e.g. adding vs deleting content. (arbitrary short string)
8+
3. the `worker` name: random string, should be the same between calls.
99

1010
Further, you may specify the `timeout` (>= 1s) after which the task should be deemed to have failed. The `task_interval` (>=5s) which is how often to do the task. (This must be longer than the `timeout`.)
1111

0 commit comments

Comments
 (0)