234 lines
6.9 KiB
TypeScript
234 lines
6.9 KiB
TypeScript
import { getKysely } from "./kysely";
|
|
import { jsonArrayFrom } from "kysely/helpers/sqlite";
|
|
import type { DocumentAccessor } from "../model/doc";
|
|
import type {
|
|
Document,
|
|
QueryListOption,
|
|
DocumentBody
|
|
} from "dbtype/api";
|
|
import type { NotNull } from "kysely";
|
|
import { MyParseJSONResultsPlugin } from "./plugin";
|
|
|
|
export type DBTagContentRelation = {
|
|
doc_id: number;
|
|
tag_name: string;
|
|
};
|
|
|
|
class SqliteDocumentAccessor implements DocumentAccessor {
|
|
constructor(private kysely = getKysely()) {
|
|
}
|
|
async search(search_word: string): Promise<Document[]> {
|
|
throw new Error("Method not implemented.");
|
|
}
|
|
async addList(content_list: DocumentBody[]): Promise<number[]> {
|
|
return await this.kysely.transaction().execute(async (trx) => {
|
|
// add tags
|
|
const tagCollected = new Set<string>();
|
|
for (const content of content_list) {
|
|
for (const tag of content.tags) {
|
|
tagCollected.add(tag);
|
|
}
|
|
}
|
|
await trx.insertInto("tags")
|
|
.values(Array.from(tagCollected).map((x) => ({ name: x })))
|
|
.onConflict((oc) => oc.doNothing())
|
|
.execute();
|
|
|
|
const ids = await trx.insertInto("document")
|
|
.values(content_list.map((content) => {
|
|
const { tags, additional, ...rest } = content;
|
|
return {
|
|
additional: JSON.stringify(additional),
|
|
created_at: Date.now(),
|
|
...rest,
|
|
};
|
|
}))
|
|
.returning("id")
|
|
.execute();
|
|
const id_lst = ids.map((x) => x.id);
|
|
|
|
const doc_tags = content_list.flatMap((content, index) => {
|
|
const { tags, ...rest } = content;
|
|
return tags.map((tag) => ({ doc_id: id_lst[index], tag_name: tag }));
|
|
});
|
|
await trx.insertInto("doc_tag_relation")
|
|
.values(doc_tags)
|
|
.execute();
|
|
return id_lst;
|
|
});
|
|
}
|
|
async add(c: DocumentBody) {
|
|
return await this.kysely.transaction().execute(async (trx) => {
|
|
const { tags, additional, ...rest } = c;
|
|
const id_lst = await trx.insertInto("document").values({
|
|
additional: JSON.stringify(additional),
|
|
created_at: Date.now(),
|
|
...rest,
|
|
})
|
|
.returning("id")
|
|
.executeTakeFirst() as { id: number };
|
|
const id = id_lst.id;
|
|
|
|
// add tags
|
|
await trx.insertInto("tags")
|
|
.values(tags.map((x) => ({ name: x })))
|
|
// on conflict is supported in sqlite and postgresql.
|
|
.onConflict((oc) => oc.doNothing())
|
|
.execute();
|
|
|
|
if (tags.length > 0) {
|
|
await trx.insertInto("doc_tag_relation")
|
|
.values(tags.map((x) => ({ doc_id: id, tag_name: x })))
|
|
.execute();
|
|
}
|
|
return id;
|
|
});
|
|
}
|
|
async del(id: number) {
|
|
// delete tags
|
|
await this.kysely
|
|
.deleteFrom("doc_tag_relation")
|
|
.where("doc_id", "=", id)
|
|
.execute();
|
|
// delete document
|
|
const result = await this.kysely
|
|
.deleteFrom("document")
|
|
.where("id", "=", id)
|
|
.executeTakeFirst();
|
|
return result.numDeletedRows > 0;
|
|
}
|
|
async findById(id: number, tagload?: boolean): Promise<Document | undefined> {
|
|
const doc = await this.kysely.selectFrom("document")
|
|
.selectAll()
|
|
.where("id", "=", id)
|
|
.$if(tagload ?? false, (qb) =>
|
|
qb.select(eb => jsonArrayFrom(
|
|
eb.selectFrom("doc_tag_relation")
|
|
.select(["doc_tag_relation.tag_name"])
|
|
.whereRef("document.id", "=", "doc_tag_relation.doc_id")
|
|
.select("tag_name")
|
|
).as("tags")).withPlugin(new MyParseJSONResultsPlugin("tags"))
|
|
)
|
|
.executeTakeFirst();
|
|
if (!doc) return undefined;
|
|
|
|
return {
|
|
...doc,
|
|
content_hash: doc.content_hash ?? "",
|
|
additional: doc.additional !== null ? JSON.parse(doc.additional) : {},
|
|
tags: doc.tags?.map((x: { tag_name: string }) => x.tag_name) ?? [],
|
|
};
|
|
}
|
|
async findDeleted(content_type: string) {
|
|
const docs = await this.kysely
|
|
.selectFrom("document")
|
|
.selectAll()
|
|
.where("content_type", "=", content_type)
|
|
.where("deleted_at", "is not", null)
|
|
.$narrowType<{ deleted_at: NotNull }>()
|
|
.execute();
|
|
return docs.map((x) => ({
|
|
...x,
|
|
tags: [],
|
|
content_hash: x.content_hash ?? "",
|
|
additional: {},
|
|
}));
|
|
}
|
|
async findList(option?: QueryListOption) {
|
|
const {
|
|
allow_tag = [],
|
|
eager_loading = true,
|
|
limit = 20,
|
|
use_offset = false,
|
|
offset = 0,
|
|
word,
|
|
content_type,
|
|
cursor,
|
|
} = option ?? {};
|
|
|
|
const result = await this.kysely
|
|
.selectFrom("document")
|
|
.selectAll()
|
|
.$if(allow_tag.length > 0, (qb) => {
|
|
return allow_tag.reduce((prevQb, tag, index) => {
|
|
return prevQb.innerJoin(`doc_tag_relation as tags_${index}`, `tags_${index}.doc_id`, "document.id")
|
|
.where(`tags_${index}.tag_name`, "=", tag);
|
|
}, qb) as unknown as typeof qb;
|
|
})
|
|
.$if(word !== undefined, (qb) => qb.where("title", "like", `%${word}%`))
|
|
.$if(content_type !== undefined, (qb) => qb.where("content_type", "=", content_type as string))
|
|
.$if(use_offset, (qb) => qb.offset(offset))
|
|
.$if(!use_offset && cursor !== undefined, (qb) => qb.where("id", "<", cursor as number))
|
|
.limit(limit)
|
|
.$if(eager_loading, (qb) => {
|
|
return qb.select(eb =>
|
|
eb.selectFrom(e =>
|
|
e.selectFrom("doc_tag_relation")
|
|
.select(["doc_tag_relation.tag_name"])
|
|
.whereRef("document.id", "=", "doc_tag_relation.doc_id")
|
|
.as("agg")
|
|
).select(e => e.fn.agg<string>("json_group_array", ["agg.tag_name"])
|
|
.as("tags_list")
|
|
).as("tags")
|
|
)
|
|
})
|
|
.orderBy("id", "desc")
|
|
.execute();
|
|
return result.map((x) => ({
|
|
...x,
|
|
content_hash: x.content_hash ?? "",
|
|
additional: x.additional !== null ? (JSON.parse(x.additional)) : {},
|
|
tags: JSON.parse(x.tags ?? "[]"), //?.map((x: { tag_name: string }) => x.tag_name) ?? [],
|
|
}));
|
|
}
|
|
async findByPath(path: string, filename?: string): Promise<Document[]> {
|
|
const results = await this.kysely
|
|
.selectFrom("document")
|
|
.selectAll()
|
|
.where("basepath", "=", path)
|
|
.$if(filename !== undefined, (qb) => qb.where("filename", "=", filename as string))
|
|
.execute();
|
|
return results.map((x) => ({
|
|
...x,
|
|
content_hash: x.content_hash ?? "",
|
|
tags: [],
|
|
additional: {},
|
|
}));
|
|
}
|
|
async update(c: Partial<Document> & { id: number }) {
|
|
const { id, tags, additional, ...rest } = c;
|
|
const r = await this.kysely.updateTable("document")
|
|
.set({
|
|
...rest,
|
|
modified_at: Date.now(),
|
|
additional: additional !== undefined ? JSON.stringify(additional) : undefined,
|
|
})
|
|
.where("id", "=", id)
|
|
.executeTakeFirst();
|
|
return r.numUpdatedRows > 0;
|
|
}
|
|
async addTag(c: Document, tag_name: string) {
|
|
if (c.tags.includes(tag_name)) return false;
|
|
await this.kysely.insertInto("tags")
|
|
.values({ name: tag_name })
|
|
.onConflict((oc) => oc.doNothing())
|
|
.execute();
|
|
await this.kysely.insertInto("doc_tag_relation")
|
|
.values({ tag_name: tag_name, doc_id: c.id })
|
|
.execute();
|
|
c.tags.push(tag_name);
|
|
return true;
|
|
}
|
|
async delTag(c: Document, tag_name: string) {
|
|
if (c.tags.includes(tag_name)) return false;
|
|
await this.kysely.deleteFrom("doc_tag_relation")
|
|
.where("tag_name", "=", tag_name)
|
|
.where("doc_id", "=", c.id)
|
|
.execute();
|
|
c.tags.splice(c.tags.indexOf(tag_name), 1);
|
|
return true;
|
|
}
|
|
}
|
|
export const createSqliteDocumentAccessor = (kysely = getKysely()): DocumentAccessor => {
|
|
return new SqliteDocumentAccessor(kysely);
|
|
};
|