desktop M10.3: frontend data-source adapter seam (repo interface + rest.ts)
Extract a typed repository interface (adapters/repo.ts) from the scattered store/view -> api.* calls, backed by adapters/rest.ts (verbatim HTTP mapping) and selected through adapters/index.ts. Every store and the notes-facing views now depend on `repo`, never the HTTP client directly -- the seam the offline local source (M10.5, over Tauri invoke) plugs into next. Behavior-preserving for web: rest.ts maps each semantic method to the exact endpoint the code called before; query-string and multipart building moved out of the stores/views into rest.ts (the one place that knows the URL shape). Client-side logic (reconcile/sort/optimistic reorder/toasts) stays in the stores. GraphView + admin SettingsView keep direct api calls -- out of the offline-core scope (M10.5 is board/editor/capture/search/filter/labels/ checklists/reminders). Task 1992. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
// REST implementation of the repository seam: maps each semantic operation to the
|
||||
// existing HTTP endpoint via `api/client`. This is the ONLY place that knows about
|
||||
// URL paths, query strings, and multipart bodies. Behaviour here must stay a
|
||||
// verbatim mirror of the calls the stores/views made before the seam existed — the
|
||||
// web app is unchanged; the offline `local.ts` source (M10.5) is the alternative.
|
||||
|
||||
import { api } from "../api/client";
|
||||
import type { Note, NoteRevision } from "../stores/notes";
|
||||
import type { Label } from "../stores/labels";
|
||||
import type { SavedFilter } from "../stores/savedFilters";
|
||||
import type { Device } from "../stores/devices";
|
||||
import type { TitleEntry } from "../stores/titles";
|
||||
import type { User } from "../stores/session";
|
||||
import type { PublicConfig } from "../stores/config";
|
||||
import type {
|
||||
Backlink,
|
||||
DeviceToken,
|
||||
ImportResult,
|
||||
NoteChanges,
|
||||
NoteCreateInput,
|
||||
NoteListQuery,
|
||||
ChecklistItemChanges,
|
||||
Repo,
|
||||
} from "./repo";
|
||||
|
||||
// Render a board query to the GET /api/notes query string. Mirrors the param
|
||||
// building that used to live in notes.store.load()/TimelineView (order-preserving;
|
||||
// query-param order is irrelevant to the server, but kept close for review).
|
||||
function notesQuery(q: NoteListQuery): string {
|
||||
const params = new URLSearchParams();
|
||||
params.set("filter", q.view);
|
||||
if (q.labelId) params.append("label", q.labelId);
|
||||
for (const id of q.facets?.label ?? []) if (id) params.append("label", id);
|
||||
if (q.facets?.q) params.set("q", q.facets.q);
|
||||
if (q.facets?.color) params.set("color", q.facets.color);
|
||||
if (q.facets?.kind) params.set("kind", q.facets.kind);
|
||||
if (q.facets?.has_reminder) params.set("has_reminder", "true");
|
||||
if (q.facets?.has_attachment) params.set("has_attachment", "true");
|
||||
if (q.facets?.created_after) params.set("created_after", q.facets.created_after);
|
||||
if (q.facets?.created_before) params.set("created_before", q.facets.created_before);
|
||||
if (q.sort) params.set("sort", q.sort);
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
function fileForm(file: File): FormData {
|
||||
const form = new FormData();
|
||||
form.append("file", file);
|
||||
return form;
|
||||
}
|
||||
|
||||
export const rest: Repo = {
|
||||
config: {
|
||||
get: () => api.get<PublicConfig>("/api/config"),
|
||||
},
|
||||
|
||||
auth: {
|
||||
me: () => api.get<User>("/api/auth/me"),
|
||||
login: (email, password) => api.post<User>("/api/auth/login", { email, password }),
|
||||
register: (email, password, displayName) =>
|
||||
api.post<User>("/api/auth/register", { email, password, display_name: displayName }),
|
||||
logout: () => api.post<void>("/api/auth/logout"),
|
||||
},
|
||||
|
||||
devices: {
|
||||
list: async () => (await api.get<{ devices: Device[] }>("/api/auth/devices")).devices,
|
||||
create: (name) => api.post<DeviceToken>("/api/auth/devices", { name }),
|
||||
remove: (id) => api.del<void>(`/api/auth/devices/${id}`),
|
||||
},
|
||||
|
||||
labels: {
|
||||
list: async () => (await api.get<{ labels: Label[] }>("/api/labels")).labels,
|
||||
create: (name) => api.post<Label>("/api/labels", { name }),
|
||||
rename: (id, name) => api.patch<Label>(`/api/labels/${id}`, { name }),
|
||||
setColor: (id, color) => api.patch<Label>(`/api/labels/${id}`, { color }),
|
||||
remove: (id) => api.del<void>(`/api/labels/${id}`),
|
||||
merge: (sourceId, into) => api.post<Label>(`/api/labels/${sourceId}/merge`, { into }),
|
||||
},
|
||||
|
||||
notes: {
|
||||
list: async (query) => (await api.get<{ notes: Note[] }>(`/api/notes?${notesQuery(query)}`)).notes,
|
||||
get: (id) => api.get<Note>(`/api/notes/${id}`),
|
||||
create: (input: NoteCreateInput) => api.post<Note>("/api/notes", input),
|
||||
createTitled: (title) => api.post<Note>("/api/notes", { title, body: "" }),
|
||||
update: (id, changes: NoteChanges) => api.patch<Note>(`/api/notes/${id}`, changes),
|
||||
completeReminder: (id) => api.post<Note>(`/api/notes/${id}/reminder/complete`),
|
||||
snoozeReminder: (id, minutes) => api.post<Note>(`/api/notes/${id}/reminder/snooze`, { minutes }),
|
||||
setLabels: (id, labelIds) => api.put<Note>(`/api/notes/${id}/labels`, { label_ids: labelIds }),
|
||||
addItem: (id, text) => api.post<Note>(`/api/notes/${id}/items`, { text }),
|
||||
updateItem: (id, itemId, changes: ChecklistItemChanges) =>
|
||||
api.patch<Note>(`/api/notes/${id}/items/${itemId}`, changes),
|
||||
deleteItem: (id, itemId) => api.del<Note>(`/api/notes/${id}/items/${itemId}`),
|
||||
uploadAttachment: (id, file) => api.postForm<Note>(`/api/notes/${id}/attachments`, fileForm(file)),
|
||||
deleteAttachment: (id, attId) => api.del<Note>(`/api/notes/${id}/attachments/${attId}`),
|
||||
unfurl: (id, url) => api.post<Note>(`/api/notes/${id}/unfurl`, { url }),
|
||||
deletePreview: (id, previewId) => api.del<Note>(`/api/notes/${id}/previews/${previewId}`),
|
||||
import: (file) => api.postForm<ImportResult>("/api/notes/import", fileForm(file)),
|
||||
reorder: (orderedIds) => api.post<void>("/api/notes/reorder", { ids: orderedIds }),
|
||||
trash: (id) => api.post<Note>(`/api/notes/${id}/trash`),
|
||||
restore: (id) => api.post<Note>(`/api/notes/${id}/restore`),
|
||||
deleteForever: (id) => api.del<void>(`/api/notes/${id}`),
|
||||
revisions: async (id) => (await api.get<{ revisions: NoteRevision[] }>(`/api/notes/${id}/revisions`)).revisions,
|
||||
restoreRevision: (id, revId) => api.post<Note>(`/api/notes/${id}/revisions/${revId}/restore`),
|
||||
reminders: async () => (await api.get<{ notes: Note[] }>("/api/notes/reminders")).notes,
|
||||
titles: async () => (await api.get<{ titles: TitleEntry[] }>("/api/notes/titles")).titles,
|
||||
search: async (q) => (await api.get<{ notes: Note[] }>(`/api/notes/search?q=${encodeURIComponent(q)}`)).notes,
|
||||
backlinks: async (id) => (await api.get<{ backlinks: Backlink[] }>(`/api/notes/${id}/backlinks`)).backlinks,
|
||||
linkSearch: async (q) =>
|
||||
(await api.get<{ results: TitleEntry[] }>(`/api/notes/link-search?q=${encodeURIComponent(q)}`)).results,
|
||||
},
|
||||
|
||||
savedFilters: {
|
||||
list: async () => (await api.get<{ filters: SavedFilter[] }>("/api/saved-filters")).filters,
|
||||
create: (name, params) => api.post<SavedFilter>("/api/saved-filters", { name, params }),
|
||||
remove: (id) => api.del<void>(`/api/saved-filters/${id}`),
|
||||
rename: (id, name) => api.patch<SavedFilter>(`/api/saved-filters/${id}`, { name }),
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user