Skip to content

Commit

Permalink
udpate note links when journal changes
Browse files Browse the repository at this point in the history
- when changing journal of note A, find all notes that point to it and update their links
  • Loading branch information
cloverich committed Dec 7, 2024
1 parent fe301bd commit 65fd544
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 38 deletions.
9 changes: 9 additions & 0 deletions src/electron/migrations/20211005142122.sql
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,16 @@ CREATE TABLE IF NOT EXISTS "document_tags" (
PRIMARY KEY ("documentId", "tag")
);

CREATE TABLE IF NOT EXISTS "document_links" (
"documentId" TEXT NOT NULL,
"targetId" TEXT NOT NULL,
"targetJournal" TEXT NOT NULL,
"resolvedAt" TEXT,
FOREIGN KEY ("documentId") REFERENCES "documents" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY ("documentId", "targetId")
);

CREATE INDEX IF NOT EXISTS "document_links_target_idx" ON "document_links"("targetId");
CREATE INDEX IF NOT EXISTS "documents_title_idx" ON "documents"("title");
CREATE INDEX IF NOT EXISTS "documents_createdat_idx" ON "documents"("createdAt");
CREATE INDEX IF NOT EXISTS "tags_name_idx" ON "document_tags"("tag");
Expand Down
29 changes: 29 additions & 0 deletions src/markdown/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,32 @@ export const stringToSlate = (input: string, parse = parseMarkdown) => {
export const slateToString = (nodes: SlateCustom.SlateNode[]) => {
return mdastToString(wrapImages(slateToMdast(nodes)));
};

// is a markdown link a link to another note?
// NOTE: Confusing name; dont have a distinct noteLink type in mdast, maybe
// should be targetsNote or something
export const isNoteLink = (mdast: mdast.RootContent): mdast is mdast.Link => {
if (mdast.type !== "link") return false;

// we are only interested in markdown links
if (!mdast.url.endsWith(".md")) return false;

// ensure its not a url with an .md domain
if (mdast.url.includes("://")) return false;

return true;
};

export const selectNoteLinks = (
mdast: mdast.Content | mdast.Root,
): mdast.Link[] => {
const links: mdast.Link[] = [];
if (mdast.type === "link" && isNoteLink(mdast)) {
links.push(mdast);
} else if ("children" in mdast) {
for (const child of mdast.children as any) {
links.push(...selectNoteLinks(child));
}
}
return links;
};
96 changes: 88 additions & 8 deletions src/preload/client/documents.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Database } from "better-sqlite3";
import { Knex } from "knex";
import { uuidv7obj } from "uuidv7";
import { mdastToString, parseMarkdown, selectNoteLinks } from "../../markdown";
import { parseNoteLink } from "../../views/edit/editor/features/note-linking/toMdast";
import { IFilesClient } from "./files";

export interface GetDocumentResponse {
Expand All @@ -13,6 +15,14 @@ export interface GetDocumentResponse {
tags: string[];
}

// table structure of document_links
interface DocumentLinkDb {
documentId: string;
targetId: string;
targetJournal: string;
resolvedAt: string; // todo: unused
}

/**
* Structure for searching journal content.
*/
Expand Down Expand Up @@ -241,7 +251,7 @@ updatedAt: ${document.updatedAt}
);

if (index) {
return [this.createIndex({ id, ...args }), docPath];
return [await this.createIndex({ id, ...args }), docPath];
} else {
return [id, docPath];
}
Expand All @@ -257,25 +267,60 @@ updatedAt: ${document.updatedAt}
if (origDoc.journal !== args.journal) {
// no await, optimistic delete
this.files.deleteDocument(args.id!, origDoc.journal);
this.updateDependentLinks([args.id!], args.journal);
}

return this.updateIndex(args);
return await this.updateIndex(args);
};

// todo: also need to update dependent title, if the title of the original note
// changes...again wikilinks simplify this.
updateDependentLinks = async (documentIds: string[], journal: string) => {
for (const targetId of documentIds) {
const links = await this.knex<DocumentLinkDb>("document_links").where({
targetId,
});

for (const link of links) {
const dependentNote = await this.findById({ id: link.documentId });
console.log("udating links for", dependentNote.title, dependentNote.id);
const mdast = parseMarkdown(dependentNote.content);
const noteLinks = selectNoteLinks(mdast);

// update the note links to point to the new journal
noteLinks.forEach((link) => {
const parsed = parseNoteLink(link.url);
if (!parsed) return;
const { noteId } = parsed;
if (noteId === targetId) {
// update url to new journal
link.url = `../${journal}/${noteId}.md`;
link.journalName = journal;
}
});

await this.save({
...dependentNote,
content: mdastToString(mdast),
});
}
}
};

createIndex = ({
createIndex = async ({
id,
createdAt,
updatedAt,
journal,
content,
title,
tags,
}: SaveRequest): string => {
}: SaveRequest): Promise<string> => {
if (!id) {
throw new Error("id required to create document index");
}

return this.db.transaction(() => {
return this.db.transaction(async () => {
this.db
.prepare(
`INSERT INTO documents (id, journal, content, title, createdAt, updatedAt) VALUES (:id, :journal, :content, :title, :createdAt, :updatedAt)`,
Expand All @@ -298,20 +343,22 @@ updatedAt: ${document.updatedAt}
.run({ documentId: id });
}

await this.addNoteLinks(id, content);

return id;
})();
};

updateIndex = ({
updateIndex = async ({
id,
createdAt,
updatedAt,
journal,
content,
title,
tags,
}: SaveRequest): void => {
return this.db.transaction(() => {
}: SaveRequest): Promise<void> => {
return this.db.transaction(async () => {
this.db
.prepare(
`UPDATE documents SET journal=:journal, content=:content, title=:title, updatedAt=:updatedAt, createdAt=:createdAt WHERE id=:id`,
Expand All @@ -325,6 +372,7 @@ updatedAt: ${document.updatedAt}
createdAt,
});

// re-create tags to avoid diffing
this.db
.prepare(`DELETE FROM document_tags WHERE documentId = :documentId`)
.run({ documentId: id });
Expand All @@ -336,9 +384,41 @@ updatedAt: ${document.updatedAt}
)
.run({ documentId: id });
}

// re-create note links to avoid diffing
await this.knex("document_links").where({ documentId: id }).del();
await this.addNoteLinks(id!, content);
})();
};

private addNoteLinks = async (documentId: string, content: string) => {
const mdast = parseMarkdown(content);
const noteLinks = selectNoteLinks(mdast)
.map((link) => parseNoteLink(link.url))
.filter(Boolean) as { noteId: string; journalName: string }[];

// drop duplicate note links, should only point to a noteId once
const seen = new Set<string>();
const noteLinksUnique = noteLinks.filter((link) => {
if (seen.has(link.noteId)) {
return false;
} else {
seen.add(link.noteId);
return true;
}
});

if (noteLinks.length > 0) {
await this.knex("document_links").insert(
noteLinksUnique.map((link) => ({
documentId,
targetId: link.noteId,
targetJournal: link.journalName,
})),
);
}
};

/**
* When removing a journal, call this to de-index all documents from that journal.
*/
Expand Down
12 changes: 11 additions & 1 deletion src/preload/client/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,17 @@ export class FilesClient {
const baseDir = this.settings.get("NOTES_DIR") as string;
const newPath = path.join(baseDir, name);

return fs.promises.mkdir(newPath);
try {
await fs.promises.mkdir(newPath, { recursive: true });
} catch (err) {
// If it already exists, good to go
// note: ts can't find this type: instanceof ErrnoException
if ((err as any).code === "EEXIST") {
return newPath;
} else {
throw err;
}
}
};

removeFolder = async (name: string) => {
Expand Down
16 changes: 3 additions & 13 deletions src/preload/client/importer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type IImporterClient = ImporterClient;

import { uuidv7obj } from "uuidv7";
import {
isNoteLink,
mdastToString,
parseMarkdownForImport as stringToMdast,
} from "../../markdown";
Expand Down Expand Up @@ -419,17 +420,6 @@ export class ImporterClient {
return linkMapping;
};

// check if a markdown link is a link to a (markdown) note
private isNoteLink = (url: string) => {
// we are only interested in markdown links
if (!url.endsWith(".md")) return false;

// ensure its not a url with an .md domain
if (url.includes("://")) return false;

return true;
};

private updateNoteLinks = async (
mdast: mdast.Root | mdast.Content,
item: StagedNote,
Expand All @@ -440,8 +430,8 @@ export class ImporterClient {
) => {
// todo: update ofmWikilink
// todo: update links that point to local files
if (mdast.type === "link" && this.isNoteLink(mdast.url)) {
const url = decodeURIComponent(mdast.url);
if (isNoteLink(mdast as mdast.RootContent)) {
const url = decodeURIComponent((mdast as mdast.Link).url);
const sourceFolderPath = path.dirname(item.sourcePath);
const sourceUrlResolved = path.resolve(sourceFolderPath, url);
const mapped = linkMapping[sourceUrlResolved];
Expand Down
15 changes: 2 additions & 13 deletions src/preload/client/importer/FilesImportResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Knex } from "knex";
import mdast from "mdast";
import path from "path";
import { uuidv7obj } from "uuidv7";
import { isNoteLink } from "../../../markdown";
import { PathStatsFile } from "../../files";
import { IFilesClient } from "../files";

Expand Down Expand Up @@ -135,25 +136,13 @@ export class FilesImportResolver {
}
};

// todo: Move this back out to importer, just copy pasted to get things working
// check if a markdown link is a link to a (markdown) note
private isNoteLink = (url: string) => {
// we are only interested in markdown links
if (!url.endsWith(".md")) return false;

// ensure its not a url with an .md domain
if (url.includes("://")) return false;

return true;
};

// Determine if an mdast node is a file link
isFileLink = (
mdast: mdast.Content | mdast.Root,
): mdast is mdast.Image | mdast.Link | mdast.OfmWikiEmbedding => {
return (
(((mdast.type === "image" || mdast.type === "link") &&
!this.isNoteLink(mdast.url)) ||
!isNoteLink(mdast)) ||
mdast.type === "ofmWikiembedding") &&
!/^(https?|mailto|#|\/|\.|tel|sms|geo|data):/.test(mdast.url)
);
Expand Down
2 changes: 2 additions & 0 deletions src/preload/client/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ updatedAt: ${document.updatedAt}
this.db.exec("delete from document_tags");
this.db.exec("delete from documents");
this.db.exec("delete from journals");
// should be automatic, from documents delete cascade
this.db.exec("delete from document_links");

const rootDir = await this.preferences.get("NOTES_DIR");

Expand Down
7 changes: 4 additions & 3 deletions src/views/edit/editor/features/note-linking/toMdast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ const noteLinkRegex = /^\..\/(?:(.+)\/)?([a-zA-Z0-9-]+)\.md$/;
/**
* Check if url conforms to the note link format.
*
* ex: `../journal/note_id.md`
* todo: fuse with isNoteLink in markdown/index.ts
*
* ex: `../journal/note_id.md`
*/
export function checkNoteLink(url: string) {
export function parseNoteLink(url: string) {
if (!url) return null;

const match = url.match(noteLinkRegex);
Expand All @@ -64,7 +65,7 @@ export function toSlateNoteLink({
deco,
children,
}: ToSlateLink) {
const res = checkNoteLink(url);
const res = parseNoteLink(url);

if (res) {
return {
Expand Down

0 comments on commit 65fd544

Please sign in to comment.