Skip to content

Commit

Permalink
shared kernel for opfs related to projects
Browse files Browse the repository at this point in the history
  • Loading branch information
Mazuh committed Jan 31, 2024
1 parent 16c79e5 commit fc287db
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 54 deletions.
1 change: 1 addition & 0 deletions babel.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ module.exports = {
alias: {
"@/components": "./src/components",
"@/lib/utils": "./src/lib/utils",
"@/services": "./src/services",
},
},
],
Expand Down
23 changes: 19 additions & 4 deletions src/features/project-workspace/ProjectWorkspacePage.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { useParams } from "wouter";
import { validate as validateUuid } from "uuid";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowLeft, faFolderOpen } from "@fortawesome/free-solid-svg-icons";
import { AppPageTemplate } from "@/components/template/AppPageTemplate";
import { Anchor, Title } from "@/components/ui/typography";
import { Project } from "@/entities/management";
import { retrieveProject } from "./opfs-project-service";

export function ProjectWorkspacePage() {
const params = useParams();
const projectUUID = validateUuid(params.uuid || "") ? params.uuid : "";

const [project, setProject] = useState<Project>();

useEffect(() => {
if (!projectUUID) {
return;
}

console.warn("TODO", projectUUID);
retrieveProject(projectUUID)
.then((retrieved) => setProject(retrieved))
.catch((error) => console.error("Error retrieving project.", error));
}, [projectUUID]);

if (!projectUUID) {
Expand All @@ -30,8 +36,12 @@ export function ProjectWorkspacePage() {
);
}

if (!project) {
return null;
}

return (
<AppPageTemplate container>
<AppPageTemplate>
<p>
<Anchor href="/">
<small>
Expand All @@ -42,7 +52,12 @@ export function ProjectWorkspacePage() {
</p>
<Title>
<FontAwesomeIcon icon={faFolderOpen} />
<span className="pl-1">Project workspace</span>
<span className="pl-1">
Project workspace:{" "}
<code className="pl-2" title={project.uuid}>
{project.name}
</code>
</span>
</Title>
<p>Wadda wadda.</p>
</AppPageTemplate>
Expand Down
1 change: 1 addition & 0 deletions src/features/project-workspace/opfs-project-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { retrieveProject } from "@/services/opfs-projects-shared-internals";
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ describe("OPFS project listing service", () => {
});

it("can remove a project by quering the specific file within the directory", async () => {
(opfsAdapters.makeOpfsMainDirAdapter as jest.Mock).mockResolvedValueOnce({
retrieveFilenames: jest
.fn()
.mockResolvedValue([
"82184240-6b29-4ae8-82f5-fbe7d1bb814a_Other random API.json",
"73aa5920-55e6-4275-b193-a9a9ad5de15d_LoL API with other persisted name.json",
]),
});

const removeMock = jest.fn();
(opfsAdapters.makeOpfsFileAdapter as jest.Mock).mockResolvedValueOnce({
remove: removeMock,
Expand All @@ -86,7 +95,8 @@ describe("OPFS project listing service", () => {
expect(result).toEqual({ uuid: "73aa5920-55e6-4275-b193-a9a9ad5de15d" });

expect(opfsAdapters.makeOpfsFileAdapter).toHaveBeenCalledWith({
filename: "73aa5920-55e6-4275-b193-a9a9ad5de15d_LoL API.json",
filename:
"73aa5920-55e6-4275-b193-a9a9ad5de15d_LoL API with other persisted name.json",
subdir: "projects",
});
expect(removeMock).toHaveBeenCalledTimes(1);
Expand Down
61 changes: 12 additions & 49 deletions src/features/projects-management/opfs-projects-listing-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
* Implements project listing service using the Origin Private File System.
*/
import { v4 as uuidv4 } from "uuid";
import { Project } from "../../entities/management";
import {
makeOpfsFileAdapter,
makeOpfsMainDirAdapter,
} from "../../services/origin-private-file-system";
import { makeOpfsMainDirAdapter } from "../../services/origin-private-file-system";
import {
ProjectListing,
ProjectListingItem,
} from "./projects-management-entities";
import {
persistProject,
getListingItemFromFilename,
removeProject,
} from "@/services/opfs-projects-shared-internals";

/**
* Based on stored filenames in the projects private directory,
Expand All @@ -26,21 +27,7 @@ export async function retrieveProjectsListing(): Promise<ProjectListing> {
});
const filenames = await opfsDir.retrieveFilenames();
return {
items: filenames.map((filename) => {
const rFilename =
/(?<uuid>[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})_(?<name>[A-Za-z ]+)\.json/;
const match = filename.match(rFilename);
if (!match || !match.groups) {
throw new Error(
`Invalid project filename (corrupted data?): ${filename}`
);
}

return {
uuid: match.groups.uuid,
name: match.groups.name,
};
}),
items: filenames.map((filename) => getListingItemFromFilename(filename)),
};
}

Expand All @@ -56,10 +43,10 @@ export async function persistNewProjectListingItem(

const item: ProjectListingItem = {
uuid: uuidv4(),
name: hygienizeProjectName(name),
name: name,
};

await doPersistProject({
await persistProject({
uuid: item.uuid,
name: item.name,
sections: [],
Expand All @@ -76,14 +63,7 @@ export async function persistNewProjectListingItem(
export async function removeProjectListingItem(
project: ProjectListingItem
): Promise<{ uuid: string }> {
const file = await makeOpfsFileAdapter<never>({
filename: getFilename(project),
subdir: PROJECTS_OPFS_SUBDIRECTORY,
});

await file.remove();

return { uuid: project.uuid };
return removeProject(project.uuid);
}

/**
Expand All @@ -106,7 +86,7 @@ export async function updateProjectListingItem(

const item: ProjectListingItem = {
uuid: updating.uuid,
name: hygienizeProjectName(updating.name),
name: updating.name,
};

const listing = await retrieveProjectsListing();
Expand All @@ -115,7 +95,7 @@ export async function updateProjectListingItem(
throw new Error("Project being updated is stale.");
}

await doPersistProject({
await persistProject({
...found,
...item,
// TODO: this is erasing previous sections and requests.
Expand All @@ -129,20 +109,3 @@ export async function updateProjectListingItem(
}

export const PROJECTS_OPFS_SUBDIRECTORY = "projects";

function hygienizeProjectName(name: string): string {
return name.trim().replace(".json", "");
}

async function doPersistProject(project: Project): Promise<void> {
const file = await makeOpfsFileAdapter<Project>({
filename: getFilename(project),
subdir: PROJECTS_OPFS_SUBDIRECTORY,
});

await file.persist(project);
}

function getFilename(item: ProjectListingItem): string {
return `${item.uuid}_${item.name}.json`;
}
127 changes: 127 additions & 0 deletions src/services/opfs-projects-shared-internals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* A shared kernel serving as dependency for two or more OPFS services
* from difference project features.
*/

import {
makeOpfsFileAdapter,
makeOpfsMainDirAdapter,
} from "./origin-private-file-system";
import { Project } from "@/entities/management";

/**
* Retrieve a project from the private file system based on its UUID.
*/
export async function retrieveProject(uuid: Project["uuid"]): Promise<Project> {
const filename = await retrieveProjectFilenameByUuid(uuid);

const file = await makeOpfsFileAdapter<Project>({
filename,
subdir: PROJECTS_OPFS_SUBDIRECTORY,
});

const content = await file.retrieve();
if (!content) {
throw new Error(`Project file content not found: ${uuid}`);
}

return content;
}

/**
* Persist a file in the projects private directory,
* its filename will be created/found based on the combination
* of its UUID and name.
*/
export async function persistProject(project: Project): Promise<void> {
const file = await makeOpfsFileAdapter<Project>({
filename: getProjectFilename(project),
subdir: PROJECTS_OPFS_SUBDIRECTORY,
});

await file.persist(project);
}

/**
* Remove a file from the projects private directory, thus
* destroying permanently the project, based only
* on its UUID.
*/
export async function removeProject(uuid: string): Promise<{ uuid: string }> {
const filename = await retrieveProjectFilenameByUuid(uuid);

const file = await makeOpfsFileAdapter<never>({
filename,
subdir: PROJECTS_OPFS_SUBDIRECTORY,
});

await file.remove();

return { uuid };
}

/**
* Retrieve all raw filenames from the projects private directory.
*/
export async function retrieveProjectsFilenames(): Promise<string[]> {
const opfsDir = await makeOpfsMainDirAdapter({
subdir: PROJECTS_OPFS_SUBDIRECTORY,
});
const filenames = await opfsDir.retrieveFilenames();
return filenames;
}

/**
* Return what should be the current filename of a project.
*
* No side effects here, there's no query on file system.
*/
export function getProjectFilename(
item: Pick<Project, "uuid" | "name">
): string {
return `${item.uuid}_${hygienizeProjectName(item.name)}.json`;
}

/**
* Build a project listing item based only on the filename.
* Each project filename is actually the whole listing item.
*
* No side effects here, there's no query on file system.
*/
export function getListingItemFromFilename(
filename: string
): Pick<Project, "uuid" | "name"> {
const rFilename =
/(?<uuid>[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})_(?<name>[A-Za-z ]+)\.json/;
const match = filename.match(rFilename);
if (!match || !match.groups) {
throw new Error(`Invalid project filename (corrupted data?): ${filename}`);
}

return {
uuid: match.groups.uuid,
name: match.groups.name,
};
}

/**
* The private subdirectory where all projects are stored.
*/
export const PROJECTS_OPFS_SUBDIRECTORY = "projects";

function hygienizeProjectName(name: string): string {
return name.trim().replace(".json", "");
}

async function retrieveProjectFilenameByUuid(uuid: string): Promise<string> {
const allFilenames = await retrieveProjectsFilenames();
const filename = allFilenames.find(
(filename) => getListingItemFromFilename(filename).uuid === uuid
);

if (!filename) {
throw new Error(`Project filename not found: ${uuid}`);
}

return filename;
}

0 comments on commit fc287db

Please sign in to comment.