feat: add coding project schema v2 migration
This commit is contained in:
180
electron/coding-projects/project-store.ts
Normal file
180
electron/coding-projects/project-store.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import path from 'node:path';
|
||||
import type { ProjectType } from '../../shared/project-config';
|
||||
import {
|
||||
createCodingProjectMetadata,
|
||||
type CodingProjectConfigV2,
|
||||
} from './project-config';
|
||||
|
||||
export interface CodingProject {
|
||||
id: string;
|
||||
path: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastOpenedAt: string;
|
||||
}
|
||||
|
||||
export interface CodingProjectStoreData {
|
||||
projects: Record<string, CodingProject>;
|
||||
activeProjectId: string | null;
|
||||
}
|
||||
|
||||
export interface CodingProjectStorage {
|
||||
read(): Promise<CodingProjectStoreData | undefined>;
|
||||
write(data: CodingProjectStoreData): Promise<void>;
|
||||
}
|
||||
|
||||
export interface CodingProjectKeyValueStore {
|
||||
get(key: string): unknown;
|
||||
set(key: string, value: CodingProjectStoreData): unknown;
|
||||
}
|
||||
|
||||
export type CodingProjectStore = ReturnType<typeof createCodingProjectStore>;
|
||||
|
||||
const DEFAULT_PROJECT_STORE_KEY = 'coding-projects-v2';
|
||||
|
||||
export function normalizeCodingProjectPath(input: string): string {
|
||||
const normalized = path.normalize(path.resolve(input.trim()));
|
||||
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
||||
}
|
||||
|
||||
export function createMemoryCodingProjectStorage(
|
||||
initial?: CodingProjectStoreData,
|
||||
): CodingProjectStorage {
|
||||
let stored = initial ? structuredClone(initial) : undefined;
|
||||
return {
|
||||
async read() {
|
||||
return stored ? structuredClone(stored) : undefined;
|
||||
},
|
||||
async write(data) {
|
||||
stored = structuredClone(data);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createCodingProjectStorageFromStore(
|
||||
store: CodingProjectKeyValueStore,
|
||||
key = DEFAULT_PROJECT_STORE_KEY,
|
||||
): CodingProjectStorage {
|
||||
return {
|
||||
async read() {
|
||||
const data = store.get(key);
|
||||
return data ? structuredClone(data) as CodingProjectStoreData : undefined;
|
||||
},
|
||||
async write(data) {
|
||||
store.set(key, structuredClone(data));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function emptyStoreData(): CodingProjectStoreData {
|
||||
return { projects: {}, activeProjectId: null };
|
||||
}
|
||||
|
||||
export function createCodingProjectStore(
|
||||
storage: CodingProjectStorage,
|
||||
options: {
|
||||
createId?: () => string;
|
||||
now?: () => string;
|
||||
} = {},
|
||||
) {
|
||||
const createId = options.createId ?? randomUUID;
|
||||
const now = options.now ?? (() => new Date().toISOString());
|
||||
let mutationTail = Promise.resolve();
|
||||
|
||||
async function readData(): Promise<CodingProjectStoreData> {
|
||||
return (await storage.read()) ?? emptyStoreData();
|
||||
}
|
||||
|
||||
function mutate<T>(operation: () => Promise<T>): Promise<T> {
|
||||
const result = mutationTail.then(operation, operation);
|
||||
mutationTail = result.then(() => undefined, () => undefined);
|
||||
return result;
|
||||
}
|
||||
|
||||
async function upsertProject(input: string, activate: boolean): Promise<CodingProject> {
|
||||
return await mutate(async () => {
|
||||
const data = await readData();
|
||||
const normalizedPath = normalizeCodingProjectPath(input);
|
||||
const existing = Object.values(data.projects)
|
||||
.find((project) => project.path === normalizedPath);
|
||||
const timestamp = now();
|
||||
const project: CodingProject = existing
|
||||
? {
|
||||
...existing,
|
||||
name: path.basename(normalizedPath),
|
||||
updatedAt: timestamp,
|
||||
lastOpenedAt: timestamp,
|
||||
}
|
||||
: {
|
||||
id: createId(),
|
||||
path: normalizedPath,
|
||||
name: path.basename(normalizedPath),
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
lastOpenedAt: timestamp,
|
||||
};
|
||||
data.projects[project.id] = project;
|
||||
if (activate) data.activeProjectId = project.id;
|
||||
await storage.write(data);
|
||||
return project;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
async openFolder(input: string): Promise<CodingProject> {
|
||||
return await upsertProject(input, true);
|
||||
},
|
||||
|
||||
async rememberProject(input: string): Promise<CodingProject> {
|
||||
return await upsertProject(input, false);
|
||||
},
|
||||
|
||||
async listProjects(): Promise<CodingProject[]> {
|
||||
await mutationTail;
|
||||
return Object.values((await readData()).projects)
|
||||
.sort((left, right) => right.lastOpenedAt.localeCompare(left.lastOpenedAt));
|
||||
},
|
||||
|
||||
async getActiveProject(): Promise<CodingProject | null> {
|
||||
await mutationTail;
|
||||
const data = await readData();
|
||||
return data.activeProjectId ? data.projects[data.activeProjectId] ?? null : null;
|
||||
},
|
||||
|
||||
async setActiveProject(projectId: string | null): Promise<CodingProject | null> {
|
||||
return await mutate(async () => {
|
||||
const data = await readData();
|
||||
data.activeProjectId = projectId && data.projects[projectId] ? projectId : null;
|
||||
await storage.write(data);
|
||||
return data.activeProjectId ? data.projects[data.activeProjectId] : null;
|
||||
});
|
||||
},
|
||||
|
||||
async removeProject(projectId: string): Promise<void> {
|
||||
await mutate(async () => {
|
||||
const data = await readData();
|
||||
delete data.projects[projectId];
|
||||
if (data.activeProjectId === projectId) data.activeProjectId = null;
|
||||
await storage.write(data);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function createLocalCodingProject(
|
||||
input: {
|
||||
projectPath: string;
|
||||
projectType?: ProjectType;
|
||||
now?: string;
|
||||
},
|
||||
store: CodingProjectStore,
|
||||
): Promise<{ project: CodingProject; config: CodingProjectConfigV2 }> {
|
||||
const config = await createCodingProjectMetadata(input.projectPath, {
|
||||
projectType: input.projectType,
|
||||
now: input.now,
|
||||
});
|
||||
const project = await store.openFolder(input.projectPath);
|
||||
return { project, config };
|
||||
}
|
||||
Reference in New Issue
Block a user