feat: Adopt HTTP apis for web deployment#393
Conversation
Codecov Report
@@ Coverage Diff @@
## main #393 +/- ##
==========================================
+ Coverage 82.65% 83.81% +1.16%
==========================================
Files 186 17 -169
Lines 10884 1168 -9716
Branches 1142 0 -1142
==========================================
- Hits 8996 979 -8017
+ Misses 1873 189 -1684
+ Partials 15 0 -15
... and 168 files with indirect coverage changes 📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
…-with-the-new-http-backend-api
…-with-the-new-http-backend-api
…-with-the-new-http-backend-api
…-with-the-new-http-backend-api
danvk
left a comment
There was a problem hiding this comment.
A few comments for you. In general we should try to avoid as much "adapter" code as we can since both the server and client are under our control. Rather than adapting the server response in client code, just change the server to return what we want.
|
|
||
| export async function getRemoteProject(projectId: string) { | ||
| const res = await fetch(`/api/projects/${projectId}`); | ||
| const data: ProjectIdResponse = res.json() as unknown as ProjectIdResponse; |
There was a problem hiding this comment.
We should adapt the codegen setup before we go too far down this path. I can take a crack at that.
There was a problem hiding this comment.
You are right. We need to iron the projects API in line with what we have discussed early (ex: stop exposing the internal server path). Created a ticket for that here.
| }, | ||
| }); | ||
| const payload = (await response.json()) as Record<string, string>; | ||
| const projectId = Object.keys(payload)[0]; |
There was a problem hiding this comment.
Let's update the Python API to produce the response type that we want.
| export async function readProjectFilesFromWeb(projectId: string) { | ||
| const projectInfo = await getRemoteProject(projectId); | ||
| const relativePaths = projectInfo.filepaths | ||
| .map((path) => path.replace(projectInfo.project_path + '/', '')) // Remove project path from API output |
There was a problem hiding this comment.
should this logic all be on the python side or here?
| // ######################################################################################## | ||
| // FILESYSTEM | ||
| // ######################################################################################## | ||
| export const writeTextFileFromWeb = async (projectId: string, filePath: string, contents: string) => { |
There was a problem hiding this comment.
export const writeTextFileFromWeb = writeRemoteTextFile;There was a problem hiding this comment.
Thanks. In the end maybe I won't have the ...FromWeb and ...Remote... versions. So I will keep this in mind.
| .filter((x) => !!x) | ||
| .join('/')) as NormalizedPath; | ||
| } | ||
| const fs = new Map<NormalizedPath, FakeNode>(); |
There was a problem hiding this comment.
Very happy that this code had such a short life :)
| export const readBinaryFile: typeof tauriFs.readBinaryFile = async (filePath) => { | ||
| const f = await readTextFile(filePath); | ||
| return new TextEncoder().encode(f); | ||
| /** MIGRATED */ |
There was a problem hiding this comment.
If this needs to stay in, you can use @deprecated to make it appear struck-through as a reminder at call sites.
There was a problem hiding this comment.
I'm sure we will remove this :-) very soon.
| return Promise.all(entries.map(convertTauriFileEntryToFileEntry)); | ||
| if (isRunningOnWeb()) { | ||
| console.log('reading file structure from web'); | ||
| const entries = await readProjectFilesFromWeb(currentProjectId); |
There was a problem hiding this comment.
Try to factor out as much shared code as you can, i.e.
let entries;
if (isRunningOnWeb()) {
entries = await readProjectFilesFromWeb(currentProjectId);
} else {
entries = await readDir(systemBaseDir, { recursive: true });
}
return Promise.all(entries.map(convertTauriFileEntryToFileEntry));the appeal of the tauri-api-stubs approach was that it naturally avoided this kind of duplication.
There was a problem hiding this comment.
Right. This time I've prioritised making the two code paths clear. I think that this will very soon be the same code for desktop and web.
| } | ||
|
|
||
| let currentProjectId = ''; | ||
| export function setCurrentProjectId(projectId: string) { |
There was a problem hiding this comment.
This is duplicative of the atom state. We should either pass the project ID to the filesystem functions or, if that involves too much repetition, we could create a FilesystemForProject class that takes it once in the constructor.
There was a problem hiding this comment.
Sharp eyes @danvk :-).
I wasn't sure (still not 100% sure) how to make the current project id available throughout the app.
I started with storing the id in the project store, but then figured out that for the filesystem calls would be much simpler to just save the currentProjectId and use it for the "web".
With the "unification" of desktop and web I think that this will be cleaner in your next review.
| # ----------- | ||
| @sidecar_api.post("/ingest") | ||
| async def http_ingest(project_id: str) -> IngestResponse: | ||
| async def http_ingest(project_id: str, request: Request) -> IngestResponse: |
There was a problem hiding this comment.
why do these handlers all need to take a request now?
There was a problem hiding this comment.
They needed before to set env vars from the headers. Now that code was moved to a middleware and I haven't removed the unused parameters. You can see that change here.
Shouldn't this be caught by the python linter? (during yarn check and git push)
…-with-the-new-http-backend-api
| # ----------- | ||
| @sidecar_api.post("/rewrite") | ||
| async def http_rewrite(req: RewriteRequest) -> RewriteResponse: | ||
| async def http_rewrite(req: RewriteRequest, request: Request) -> RewriteResponse: |
There was a problem hiding this comment.
What is the second request parameter and why do we need it? It seems we don't use it anyway
There was a problem hiding this comment.
Good catch. We don't need it anymore. Removing....
| const response = await callSidecar('ingest', { pdf_directory: String(uploadsDir) }); | ||
| return parsePdfIngestionResponse(response); | ||
| export async function runPDFIngestion(projectId?: string): Promise<ReferenceItem[]> { | ||
| if (import.meta.env.VITE_IS_WEB) { |
There was a problem hiding this comment.
I don't really like having to make this check in each function. I would rather define an IngestionAPI abstract interface, with the methods we need, and then create 2 implementations WebIngestionAPI and SidecarIngestionAPI of this interface and export the correct implementation depending on VITE_IS_WEB
There was a problem hiding this comment.
I haven't implement that bc we will remove the calls via sidecar for the references/ingestion process. Maybe I should make them into this branch actually!
| export async function runPDFIngestion(projectId?: string): Promise<ReferenceItem[]> { | ||
| if (import.meta.env.VITE_IS_WEB) { | ||
| if (!projectId) { | ||
| throw new Error('Project ID is required for web version'); |
There was a problem hiding this comment.
Couldn't we make the parameter mandatory instead? The frontend should not depend on the implementation and should send an id in any case, so it's not useful to make it optional, even if we don't use it in the sidecar implementation
There was a problem hiding this comment.
Yes. That will be implemented in the follow-up PR with the unified desktop+web code to use HTTP.
| : await save({ defaultPath: await getNewProjectsBaseDir() }); | ||
| if (typeof newProjectPath === 'string') { | ||
| await newProject(newProjectPath); | ||
| if (import.meta.env.VITE_IS_WEB) { |
There was a problem hiding this comment.
nit: Maybe this logic shouldn't live in this file
…-with-the-new-http-backend-api
|
|
||
|
|
||
| @references_api.put("/{project_id}/{reference_id}") | ||
| @references_api.patch("/{project_id}/{reference_id}") |
There was a problem hiding this comment.
@gjreda FYI I've updated this to PATCH.
| export const writeBinaryFile = import.meta.env.VITE_IS_WEB ? stubFs.writeBinaryFile : tauriFs.writeBinaryFile; | ||
| export const renameFile = import.meta.env.VITE_IS_WEB ? stubFs.renameFile : tauriFs.renameFile; | ||
| export const createDir = import.meta.env.VITE_IS_WEB | ||
| ? dontUseFsInWeb<ReturnType<typeof tauriFs.createDir>> |
There was a problem hiding this comment.
This is just a sanity check. We will remove this in the follow-up PR.
| const file = new File([content], fileName, { type: 'text/plain' }); | ||
| data.append('file', file); | ||
|
|
||
| await fetch(`/api/fs/${projectId}/${filePath}`, { |
There was a problem hiding this comment.
You are right. As this PR was exploratory, I didn't implemented those validations. Will make sure we have an issue to fix this within the context of #302 .
| // ------------- | ||
| // Web | ||
| // ------------- | ||
| export const newWebProjectAtom = atom(null, async (_, set, projectId: string, path: string, name: string) => { |
There was a problem hiding this comment.
This will be unified with the desktop version in the follow-up PR.
Part of work for #347 and closes #381
This PR connect the client code (web hosting) to backend APIs via (external) HTTP using standard
fetch.A follow-up PR will existing after #417 is merged, that will implement the unified codebase for the desktop+web client code.
It's also important to note that for this PR the goal was to not change the existing desktop behaviour, implementing if/else checks with
import.meta.env.VITE_IS_WEB.For this PR the main changes where:
fs.tsand stub code. Now the code throws exception.projectIdin the client to use in every HTTP call.web.tsand*API.tsutilities to perform calls to the backend HTTP in a web context.projectIdto every business logic call in order to support listing, opening, upload files, and also references operations (ingestion, list, delete, edit).Notes: