Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add document tagging #186

Merged
merged 1 commit into from
Jun 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions scripts/dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ async function watchRenderer() {
bundle: true,
platform: "browser",
plugins: [startElectronPlugin("renderer")],
sourcemap: true,
});

await ctxRenderer.watch();
Expand All @@ -109,6 +110,7 @@ async function watchPreload() {
platform: "node",
external: ["knex", "electron", "electron-store", "better-sqlite3"],
plugins: [startElectronPlugin("preload")],
sourcemap: true,
});

await ctxPreload.watch();
Expand Down
16 changes: 11 additions & 5 deletions src/electron/migrations/20211005142122.sql
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,17 @@ CREATE TABLE IF NOT EXISTS "documents" (
FOREIGN KEY ("journalId") REFERENCES "journals" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);

-- CreateIndex
CREATE UNIQUE INDEX IF NOT EXISTS "journals_name_uniq" ON "journals"("name");

-- CreateIndex
CREATE INDEX IF NOT EXISTS "documents_title_idx" ON "documents"("title");
-- CreateTable
CREATE TABLE IF NOT EXISTS "document_tags" (
"documentId" TEXT NOT NULL,
"tag" TEXT NOT NULL,
FOREIGN KEY ("documentId") REFERENCES "documents" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY ("documentId", "tag")
);

-- CreateIndex

CREATE UNIQUE INDEX IF NOT EXISTS "journals_name_uniq" ON "journals"("name");
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");
39 changes: 39 additions & 0 deletions src/hooks/useTags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from "react";
import useClient from "./useClient";

/**
* Hook for loading tags.
*/
export function useTags() {
const [loading, setLoading] = React.useState(true);
const [tags, setTags] = React.useState<string[]>([]);
const [error, setError] = React.useState(null);
const client = useClient();

React.useEffect(() => {
let isEffectMounted = true;
setLoading(true);

async function load() {
try {
const tags = await client.tags.all();
if (!isEffectMounted) return;

setTags(tags);
setLoading(false);
} catch (err: any) {
if (!isEffectMounted) return;

setError(err);
setLoading(false);
}
}

load();
return () => {
isEffectMounted = false;
};
}, []);

return { loading, tags, error };
}
14 changes: 14 additions & 0 deletions src/markdown/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ import { Node as SNode } from "slate";

export * from "ts-mdast";

// I usually forget how unified works, so just leaving some notes for reference
// https://github.com/orgs/unifiedjs/discussions/113
// | ........................ process ........................... |
// | .......... parse ... | ... run ... | ... stringify ..........|
//
// +--------+ +----------+
// Input ->- | Parser | ->- Syntax Tree ->- | Compiler | ->- Output
// +--------+ | +----------+
// X
// |
// +--------------+
// | Transformers |
// +--------------+

const stringifier = unified().use(remarkStringify);
const parser = unified().use(remarkParse).use(remarkGfm);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { mdastToSlate } from "../transformers/mdast-to-slate";

export default function plugin() {
export default function remarkToSlatePlugin() {
// @ts-ignore
this.Compiler = function (node: any) {
return mdastToSlate(node);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import { slateToMdast } from "../transformers/slate-to-mdast";

type Settings = {};

const plugin: Plugin<[Settings?]> = function (settings?: Settings) {
const slateToRemarkPlugin: Plugin<[Settings?]> = function (
settings?: Settings,
) {
// @ts-ignore
return function (node: any) {
return slateToMdast(node);
};
};
export default plugin;

export default slateToRemarkPlugin;
164 changes: 103 additions & 61 deletions src/preload/client/documents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface GetDocumentResponse {
title?: string;
content: string;
journalId: string;
tags: string[];
}

/**
Expand Down Expand Up @@ -36,6 +37,11 @@ export interface SearchRequest {
*/
texts?: string[];

/**
* Search document #tags. ex: ['mytag', 'othertag']
*/
tags?: string[];

limit?: number;

nodeMatch?: {
Expand Down Expand Up @@ -64,7 +70,6 @@ export interface SearchItem {
journalId: string;
}

// Now straight up copying from the API layer
export interface SaveRawRequest {
journalName: string;
date: string;
Expand All @@ -84,6 +89,7 @@ export interface SaveRequest {
journalId: string;
content: string;
title?: string;
tags: string[];

// these included for override, originally,
// to support the import process
Expand All @@ -99,32 +105,38 @@ export class DocumentsClient {
private knex: Knex,
) {}

findById = ({
documentId,
}: {
documentId: string;
}): Promise<GetDocumentResponse> => {
const doc = this.db
.prepare("select * from documents where id = :id")
.get({ id: documentId });

return doc;
findById = ({ id }: { id: string }): Promise<GetDocumentResponse> => {
const document = this.db
.prepare(`SELECT * FROM documents WHERE id = :id`)
.get({ id });
const documentTags = this.db
.prepare(`SELECT tag FROM document_tags WHERE documentId = :documentId`)
.all({ documentId: id })
.map((row) => row.tag);
return {
...document,
tags: documentTags,
};
};

del = async (id: string) => {
this.db.prepare("delete from documents where id = :id").run({ id });
};

search = async (q?: SearchRequest): Promise<SearchResponse> => {
// todo: consider using raw and getting arrays of values rather than
// objects for each row, for performance
let query = this.knex("documents");

// filter by journal
if (q?.journals?.length) {
query = query.whereIn("journalId", q.journals);
}

if (q?.tags?.length) {
query = query
.join("document_tags", "documents.id", "document_tags.documentId")
.whereIn("document_tags.tag", q.tags);
}

// filter by title
if (q?.titles?.length) {
for (const title of q.titles) {
Expand Down Expand Up @@ -166,65 +178,95 @@ export class DocumentsClient {
return { data: [] };
};

save = ({
id,
/**
* Create or update a document and its tags
*
* todo: test; for tags: test prefix is removed, spaces are _, lowercased, max length
* todo: test description max length
*
* @returns - The document as it exists after the save
*/
save = (args: SaveRequest): Promise<GetDocumentResponse> => {
// de-dupe tags -- should happen before getting here.
args.tags = Array.from(new Set(args.tags));

const id = this.db.transaction(() => {
if (args.id) {
this.updateDocument(args);
return args.id;
} else {
return this.createDocument(args);
}
})();

return this.findById({ id });
};

private createDocument = ({
createdAt,
updatedAt,
journalId,
content,
title,
}: SaveRequest): Promise<GetDocumentResponse> => {
if (id) {
tags,
}: SaveRequest): string => {
const id = uuidv7();
this.db
.prepare(
`INSERT INTO documents (id, journalId, content, title, createdAt, updatedAt) VALUES (:id, :journalId, :content, :title, :createdAt, :updatedAt)`,
)
.run({
id,
journalId,
content,
title,
// allow passing createdAt to support backfilling prior notes
createdAt: createdAt || new Date().toISOString(),
updatedAt: new Date().toISOString(),
});

if (tags.length > 0) {
this.db
.prepare(
`
update documents set
journalId=:journalId,
content=:content,
title=:title,
updatedAt=:updatedAt,
createdAt=:createdAt
where
id=:id
`,
)
.run({
id,
content,
title,
journalId,
updatedAt: new Date().toISOString(),
createdAt,
});

return this.db
.prepare(
`select * from documents where id = :id order by "createdAt" desc`,
`INSERT INTO document_tags (documentId, tag) VALUES ${tags.map((tag) => `(:documentId, '${tag}')`).join(", ")}`,
)
.get({ id });
} else {
const id = uuidv7();
.run({ documentId: id });
}

return id;
};

private updateDocument = ({
id,
createdAt,
journalId,
content,
title,
tags,
}: SaveRequest): void => {
this.db
.prepare(
`UPDATE documents SET journalId=:journalId, content=:content, title=:title, updatedAt=:updatedAt, createdAt=:createdAt WHERE id=:id`,
)
.run({
id,
content,
title,
journalId,
updatedAt: new Date().toISOString(),
createdAt,
});

this.db
.prepare(`DELETE FROM document_tags WHERE documentId = :documentId`)
.run({ documentId: id });

if (tags.length > 0) {
this.db
.prepare(
`
insert into documents (id, journalId, content, title, createdAt, updatedAt)
values (:id, :journalId, :content, :title, :createdAt, :updatedAt)
`,
)
.run({
id,
journalId,
content,
title,
createdAt: createdAt || new Date().toISOString(),
updatedAt: updatedAt || new Date().toISOString(),
});

return this.db
.prepare(
`select * from documents where id = :id order by "createdAt" desc`,
`INSERT INTO document_tags (documentId, tag) VALUES ${tags.map((tag) => `(:documentId, '${tag}')`).join(", ")}`,
)
.get({ id });
.run({ documentId: id });
}
};

Expand Down
2 changes: 2 additions & 0 deletions src/preload/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { JournalsClient } from "./journals";
import { DocumentsClient } from "./documents";
import { TagsClient } from "./tags";
import { PreferencesClient } from "./preferences";
import { FilesClient } from "./files";
import { IClient } from "./types";
Expand Down Expand Up @@ -33,6 +34,7 @@ export function create(): IClient {
client = {
journals: new JournalsClient(db),
documents: new DocumentsClient(db, knex),
tags: new TagsClient(db, knex),
preferences: new PreferencesClient(settings),
files: new FilesClient(settings),
};
Expand Down
18 changes: 18 additions & 0 deletions src/preload/client/tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Database } from "better-sqlite3";
import { Knex } from "knex";

export type ITagsClient = TagsClient;

export class TagsClient {
constructor(
private db: Database,
private knex: Knex,
) {}

all = async (): Promise<string[]> => {
return this.db
.prepare(`SELECT DISTINCT tag FROM document_tags`)
.all()
.map((row) => row.tag);
};
}
Loading
Loading