Skip to content

Commit

Permalink
Migrate projects
Browse files Browse the repository at this point in the history
  • Loading branch information
eyeseast committed Sep 24, 2024
1 parent ba1cfa2 commit 6e6c1e7
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 210 deletions.
148 changes: 35 additions & 113 deletions src/lib/api/projects.ts
Original file line number Diff line number Diff line change
@@ -1,77 +1,57 @@
// api methods for projects
import type { Page } from "@/api/types";
import type {
APIError,
APIResponse,
Document,
Project,
ProjectMembershipItem,
ProjectResults,
ValidationError,
} from "./types";

import { APP_URL, BASE_API_URL, CSRF_HEADER_NAME } from "@/config/config.js";
import { getAll, isErrorCode } from "$lib/utils/api";
import { getAll, getApiResponse, isErrorCode } from "$lib/utils/api";

/**
* Get a single project
*
* @export
*/
export async function get(
id: number,
fetch = globalThis.fetch,
): Promise<Project> {
): Promise<APIResponse<Project, unknown>> {
const endpoint = new URL(`projects/${id}/`, BASE_API_URL);

const resp = await fetch(endpoint, { credentials: "include" }).catch(
console.error,
);

if (!resp) {
throw new Error("API error");
}
const resp = await fetch(endpoint, { credentials: "include" });

if (isErrorCode(resp.status)) {
throw new Error(resp.statusText);
}

return resp.json();
return getApiResponse<Project>(resp);
}

/**
* Get a page of projects
*
* @export
*/
export async function list(
params: Record<string, any> = {},
fetch = globalThis.fetch,
): Promise<ProjectResults> {
): Promise<APIResponse<ProjectResults, unknown>> {
const endpoint = new URL("projects/", BASE_API_URL);

for (const [k, v] of Object.entries(params)) {
endpoint.searchParams.set(k, String(v));
}

const resp = await fetch(endpoint, { credentials: "include" }).catch(
console.error,
);

if (!resp) {
throw new Error("API error");
}
const resp = await fetch(endpoint, { credentials: "include" });

if (isErrorCode(resp.status)) {
throw new Error(resp.statusText);
}

return resp.json();
return getApiResponse<ProjectResults>(resp);
}

/**
* Get all projects a user has access to -- owned or shared
*/
export async function getForUser(
userId: number,
query?: string,
fetch = globalThis.fetch,
) {
): Promise<Project[]> {
const endpoint = new URL("projects/", BASE_API_URL);
endpoint.searchParams.set("user", String(userId));
if (query) {
Expand Down Expand Up @@ -114,7 +94,7 @@ export async function pinProject(
pinned = true,
csrf_token: string,
fetch = globalThis.fetch,
): Promise<Project> {
) {
const endpoint = new URL(`projects/${id}/`, BASE_API_URL);
const options: RequestInit = {
credentials: "include",
Expand All @@ -129,26 +109,15 @@ export async function pinProject(
const resp = await fetch(endpoint, {
...options,
body: JSON.stringify({ pinned }),
}).catch(console.error);

if (!resp) {
throw new Error("API error");
}

if (isErrorCode(resp.status)) {
throw new Error(resp.statusText);
}
});

return resp.json();
return getApiResponse<Project>(resp);
}

// writable methods

/**
* Create a new project
*
* @param project
* @param csrf_token
* @param fetch
*/
export async function create(
project: {
Expand All @@ -159,7 +128,7 @@ export async function create(
},
csrf_token: string,
fetch = globalThis.fetch,
) {
): Promise<APIResponse<Project, ValidationError>> {
const endpoint = new URL("projects/", BASE_API_URL);
const resp = await fetch(endpoint, {
body: JSON.stringify(project),
Expand All @@ -170,26 +139,17 @@ export async function create(
Referer: APP_URL,
},
method: "POST",
}).catch(console.error);

if (!resp) {
throw new Error("API unavailable");
}

if (isErrorCode(resp.status)) {
const { data } = await resp.json();
throw new Error(data);
}
});

return resp.json();
return getApiResponse<Project, ValidationError>(resp);
}

export async function edit(
project_id: number,
data: Partial<Project>,
csrf_token: string,
fetch = globalThis.fetch,
) {
): Promise<APIResponse<Project, ValidationError>> {
const endpoint = new URL(`projects/${project_id}/`, BASE_API_URL);

const resp = await fetch(endpoint, {
Expand All @@ -201,26 +161,13 @@ export async function edit(
Referer: APP_URL,
},
method: "PATCH",
}).catch(console.error);

if (!resp) {
throw new Error("API unavailable");
}

if (isErrorCode(resp.status)) {
const { data } = await resp.json();
throw new Error(data);
}
});

return resp.json();
return getApiResponse<Project, ValidationError>(resp);
}

/**
* Delete a project. There is no undo.
*
* @param project_id
* @param csrf_token
* @param fetch
*/
export async function destroy(
project_id: number,
Expand All @@ -229,7 +176,7 @@ export async function destroy(
) {
const endpoint = new URL(`projects/${project_id}/`, BASE_API_URL);

return fetch(endpoint, {
const resp = await fetch(endpoint, {
credentials: "include",
headers: {
"Content-type": "application/json",
Expand All @@ -238,6 +185,8 @@ export async function destroy(
},
method: "DELETE",
});

return getApiResponse<null>(resp);
}

/**
Expand All @@ -253,7 +202,7 @@ export async function add(
documents: (string | number)[],
csrf_token: string,
fetch = globalThis.fetch,
): Promise<ProjectMembershipItem[] | APIError> {
): Promise<APIResponse<ProjectMembershipItem[], unknown>> {
const endpoint = new URL(`projects/${project_id}/documents/`, BASE_API_URL);
const data = documents.map((document) => ({ document }));
const resp = await fetch(endpoint, {
Expand All @@ -265,32 +214,20 @@ export async function add(
Referer: APP_URL,
},
method: "POST",
}).catch(console.error);

if (!resp) {
throw new Error("API unavailable");
}

// trying out some new error handling
if (isErrorCode(resp.status)) {
return {
error: {
status: resp.status,
message: resp.statusText,
...(await resp.json()),
},
};
}
});

return resp.json();
return getApiResponse<ProjectMembershipItem[]>(resp);
}

/**
* Remove documents from a project
*/
export async function remove(
project_id: number,
documents: (string | number)[],
csrf_token: string,
fetch = globalThis.fetch,
): Promise<APIError | null> {
) {
const endpoint = new URL(`projects/${project_id}/documents/`, BASE_API_URL);
endpoint.searchParams.set("document_id__in", documents.join(","));

Expand All @@ -302,24 +239,9 @@ export async function remove(
Referer: APP_URL,
},
method: "DELETE",
}).catch(console.error);

if (!resp) {
throw new Error("API unavailable");
}

// trying out some new error handling
if (isErrorCode(resp.status)) {
return {
error: {
status: resp.status,
message: resp.statusText,
...(await resp.json()),
},
};
}
});

return null;
return getApiResponse<null>(resp);
}

/**
Expand Down
34 changes: 27 additions & 7 deletions src/lib/api/tests/projects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,14 @@ describe("projects.get", () => {
vi.restoreAllMocks();
});
it("fetches a single project by ID", async () => {
let res = await projects.get(1, mockFetch);
const { data: res, error } = await projects.get(1, mockFetch);
expect(mockFetch).toHaveBeenCalledWith(
new URL(`${BASE_API_URL}projects/1/`),
{
credentials: "include",
},
);
expect(error).toBeUndefined();
expect(res).toBe(project);
});
it("throws a 500 error if fetch fails", async () => {
Expand Down Expand Up @@ -74,11 +75,13 @@ describe("projects.list", () => {
vi.restoreAllMocks();
});
it("fetches a list of projects", async () => {
const res = await projects.list({}, mockFetch);
const { data: res, error } = await projects.list({}, mockFetch);

expect(mockFetch).toHaveBeenCalledWith(
new URL("projects/", BASE_API_URL),
expect.any(Object),
);
expect(error).toBeUndefined();
expect(res).toBe(projectList);
});
it("attaches any params to the URL as searchParams", async () => {
Expand Down Expand Up @@ -255,8 +258,13 @@ describe("project lifecycle", () => {
pinned: project.pinned,
};

const created = await projects.create(data, "token", mockFetch);
const { data: created, error } = await projects.create(
data,
"token",
mockFetch,
);

expect(error).toBeUndefined();
expect(created).toMatchObject(project);
expect(mockFetch).toBeCalledWith(new URL("projects/", BASE_API_URL), {
body: JSON.stringify(data),
Expand Down Expand Up @@ -284,8 +292,14 @@ describe("project lifecycle", () => {

const update: Partial<Project> = { title: "New title" };

const updated = await projects.edit(project.id, update, "token", mockFetch);
const { data: updated, error } = await projects.edit(
project.id,
update,
"token",
mockFetch,
);

expect(error).toBeUndefined();
expect(updated).toMatchObject({ ...project, ...update });
expect(mockFetch).toHaveBeenCalledWith(
new URL(`projects/${project.id}/`, BASE_API_URL),
Expand All @@ -310,9 +324,9 @@ describe("project lifecycle", () => {
};
});

const resp = await projects.destroy(project.id, "token", mockFetch);
const { error } = await projects.destroy(project.id, "token", mockFetch);

expect(resp.status).toEqual(204);
expect(error).toBeUndefined();
expect(mockFetch).toHaveBeenCalledWith(
new URL(`projects/${project.id}/`, BASE_API_URL),
{
Expand Down Expand Up @@ -359,8 +373,14 @@ describe("manage project documents", () => {
});

const ids = documents.results.map((d) => d.document as number);
const docs = await projects.add(1, ids, "token", mockFetch);
const { data: docs, error } = await projects.add(
1,
ids,
"token",
mockFetch,
);

expect(error).toBeUndefined();
expect(docs).toMatchObject(documents.results);
expect(mockFetch).toBeCalledWith(
new URL("projects/1/documents/", BASE_API_URL),
Expand Down
Loading

0 comments on commit 6e6c1e7

Please sign in to comment.