plan for ssg
This commit is contained in:
1 parent
a8867be72c
commit
fb69b2710f
73 files changed
+921
-718
No files matched your search
+58
-251
@@ -1,280 +1,87 @@
|
||||
import sqlite3 from 'better-sqlite3'
|
||||
/* eslint-disable import/no-mutable-exports */
|
||||
import Loki, { Collection } from 'lokijs'
|
||||
import * as z from 'zod'
|
||||
|
||||
import { builtPath } from './dir'
|
||||
import { buildPath } from './dir'
|
||||
|
||||
const zMatter = z.object({
|
||||
date: z.date().optional(),
|
||||
createdAt: z.date().optional(),
|
||||
updatedAt: z.date().optional(),
|
||||
const _zMatter = {
|
||||
priority: z.number().optional(),
|
||||
title: z.string(),
|
||||
image: z.string().optional(),
|
||||
tag: z.array(z.string()).optional(),
|
||||
category: z.string().optional(),
|
||||
category: z.array(z.string()).optional(),
|
||||
}
|
||||
|
||||
export const zMatter = z.object({
|
||||
..._zMatter,
|
||||
date: z.date().optional(),
|
||||
createdAt: z.date().optional(),
|
||||
updatedAt: z.date().optional(),
|
||||
})
|
||||
|
||||
export type IMatter = z.infer<typeof zMatter>
|
||||
|
||||
const zMatterSerialized = z.object({
|
||||
..._zMatter,
|
||||
date: z.number().optional(),
|
||||
createdAt: z.number().optional(),
|
||||
updatedAt: z.number().optional(),
|
||||
})
|
||||
|
||||
export type IMatterSerialized = z.infer<typeof zMatterSerialized>
|
||||
|
||||
export const zPost = z
|
||||
.object({
|
||||
path: z.string(),
|
||||
/**
|
||||
* Cleaned and jieba-separated markdown
|
||||
*/
|
||||
content: z.string(),
|
||||
excerptHtml: z.string(),
|
||||
contentHtml: z.string(),
|
||||
})
|
||||
.merge(zMatter.nonstrict())
|
||||
.merge(zMatterSerialized)
|
||||
|
||||
export type IPost = z.infer<typeof zPost>
|
||||
|
||||
export class Database {
|
||||
db: sqlite3.Database
|
||||
export interface IPostSearch {
|
||||
path: string
|
||||
title: string
|
||||
tag?: string[]
|
||||
category?: string[]
|
||||
/**
|
||||
* Cleaned and jieba-separated markdown
|
||||
*/
|
||||
content: string
|
||||
}
|
||||
|
||||
constructor(opts?: sqlite3.Options) {
|
||||
this.db = sqlite3(builtPath('data.db'), opts)
|
||||
export let db: Loki
|
||||
|
||||
export let dbPost: Collection<IPost>
|
||||
|
||||
export async function initDatabase() {
|
||||
if (db) {
|
||||
return
|
||||
}
|
||||
|
||||
public init(): void {
|
||||
this.db.exec(/* sql */ `
|
||||
CREATE TABLE IF NOT EXISTS files (
|
||||
[path] TEXT PRIMARY KEY,
|
||||
[date] INT, -- epoch milli
|
||||
createdAt INT, -- epoch milli
|
||||
updatedAt INT, -- epoch milli
|
||||
priority INT,
|
||||
-- q.title
|
||||
[image] TEXT,
|
||||
-- m2o files_tag
|
||||
-- q.category
|
||||
-- q.content
|
||||
excerptHtml TEXT NOT NULL,
|
||||
contentHtml TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS files_date ON files([date]);
|
||||
CREATE INDEX IF NOT EXISTS files_createdAt ON files(createdAt);
|
||||
CREATE INDEX IF NOT EXISTS files_updatedAt ON files(updatedAt);
|
||||
CREATE INDEX IF NOT EXISTS files_priority ON files(priority);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS files_tag (
|
||||
[path] TEXT NOT NULL,
|
||||
tag TEXT NOT NULL,
|
||||
PRIMARY KEY ([path], tag)
|
||||
);
|
||||
`)
|
||||
|
||||
this.db.exec(/* sql */ `
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS q USING fts5 (
|
||||
[path], -- REFERENCES files([path])
|
||||
title,
|
||||
tag, -- space separated
|
||||
category, -- can compare via equal
|
||||
content -- cleaned markdown
|
||||
);
|
||||
`)
|
||||
}
|
||||
|
||||
public insert(...ps: IPost[]): void {
|
||||
const fStmt = this.db.prepare(/* sql */ `
|
||||
INSERT INTO files ([path], [date], [createdAt], [updatedAt], priority, [image], excerptHtml, contentHtml)
|
||||
VALUES (@path, @date, @createdAt, @updatedAt, @priority, @image, @excerptHtml, @contentHtml)
|
||||
`)
|
||||
|
||||
const ftStmt = this.db.prepare(/* sql */ `
|
||||
INSERT INTO files_tag ([path], tag)
|
||||
VALUES (@path, @tag)
|
||||
`)
|
||||
|
||||
const qStmt = this.db.prepare(/* sql */ `
|
||||
INSERT INTO q ([path], title, tag, category, content)
|
||||
VALUES (@path, @title, @tag, @category, @content)
|
||||
`)
|
||||
|
||||
this.db.transaction(() => {
|
||||
// eslint-disable-next-line array-callback-return
|
||||
ps.map((p) => {
|
||||
const {
|
||||
path,
|
||||
date,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
priority,
|
||||
image,
|
||||
excerptHtml,
|
||||
contentHtml,
|
||||
title,
|
||||
tag,
|
||||
category,
|
||||
content,
|
||||
} = zPost.parse(p)
|
||||
|
||||
fStmt.run({
|
||||
path,
|
||||
priority,
|
||||
image,
|
||||
excerptHtml,
|
||||
contentHtml,
|
||||
date: date ? date.getTime() : null,
|
||||
createdAt: createdAt ? createdAt.getTime() : null,
|
||||
updatedAt: updatedAt ? updatedAt.getTime() : null,
|
||||
})
|
||||
|
||||
qStmt.run({
|
||||
path,
|
||||
title,
|
||||
category,
|
||||
content,
|
||||
tag: tag ? tag.join(' ') : undefined,
|
||||
})
|
||||
|
||||
if (tag) {
|
||||
Array.from(new Set(tag)).map((t) => {
|
||||
ftStmt.run({
|
||||
path,
|
||||
tag: t,
|
||||
})
|
||||
return new Promise<void>((resolve) => {
|
||||
db = new Loki(buildPath('data.loki'), {
|
||||
autoload: true,
|
||||
autoloadCallback: () => {
|
||||
dbPost = db.getCollection('post')
|
||||
if (!dbPost) {
|
||||
dbPost = db.addCollection('post', {
|
||||
unique: ['path'],
|
||||
indices: [
|
||||
'date',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'priority',
|
||||
'tag',
|
||||
'category',
|
||||
],
|
||||
})
|
||||
}
|
||||
})
|
||||
})()
|
||||
}
|
||||
|
||||
public get(id: string) {
|
||||
const r = this.db
|
||||
.prepare(
|
||||
/* sql */ `
|
||||
SELECT
|
||||
[date],
|
||||
title,
|
||||
[image],
|
||||
GROUP_CONCAT(ft.tag, ' ') tag,
|
||||
category,
|
||||
contentHtml content
|
||||
FROM files f
|
||||
JOIN q ON q.path = f.path
|
||||
LEFT JOIN files_tag ft ON ft.path = f.path
|
||||
WHERE f.path = @id
|
||||
GROUP BY f.path
|
||||
`
|
||||
)
|
||||
.get({ id })
|
||||
|
||||
if (!r) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
...r,
|
||||
date: r.date ? new Date(r.date) : undefined,
|
||||
tag: r.tag ? r.tag.split(/ /g) : undefined,
|
||||
} as {
|
||||
date?: Date
|
||||
title: string
|
||||
image?: string
|
||||
tag?: string[]
|
||||
category?: string
|
||||
content: string
|
||||
}
|
||||
}
|
||||
|
||||
public q(
|
||||
q: string,
|
||||
opts: {
|
||||
offset: number
|
||||
limit: number
|
||||
tag?: string
|
||||
}
|
||||
) {
|
||||
return this._search(q, {
|
||||
...opts,
|
||||
orderedBy: 'RANK',
|
||||
resolve()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
public by(opts: { tag?: string; offset: number; limit: number }) {
|
||||
return this._search('', {
|
||||
...opts,
|
||||
orderedBy: 'priority DESC, [date] DESC, createdAt DESC, updatedAt DESC',
|
||||
where: ['[date] IS NOT NULL'],
|
||||
})
|
||||
}
|
||||
|
||||
private _search(
|
||||
q: string,
|
||||
opts: {
|
||||
offset: number
|
||||
limit: number
|
||||
orderedBy: string
|
||||
tag?: string
|
||||
where?: string[]
|
||||
}
|
||||
) {
|
||||
const $where: string[] = []
|
||||
if (q) {
|
||||
$where.push('q = @q')
|
||||
}
|
||||
|
||||
if (opts.tag) {
|
||||
$where.push('ft.tag = @tag')
|
||||
}
|
||||
|
||||
if (opts.where) {
|
||||
$where.push(...opts.where)
|
||||
}
|
||||
|
||||
const fromSegment = /* sql */ `
|
||||
FROM files f
|
||||
JOIN q ON q.path = f.path
|
||||
${opts.tag ? `LEFT JOIN files_tag ft ON ft.path = f.path` : ''}
|
||||
WHERE ${$where.join(' AND ') || 'TRUE'}
|
||||
${opts.tag ? 'GROUP BY f.path' : ''}
|
||||
`
|
||||
|
||||
const cond = {
|
||||
q,
|
||||
tag: opts.tag,
|
||||
}
|
||||
|
||||
const count =
|
||||
this.db
|
||||
.prepare(
|
||||
/* sql */ `
|
||||
SELECT COUNT(f.path) [count]
|
||||
${fromSegment}
|
||||
`
|
||||
)
|
||||
.get(cond)?.count || 0
|
||||
|
||||
const result = this.db
|
||||
.prepare(
|
||||
/* sql */ `
|
||||
SELECT
|
||||
f.path [path],
|
||||
[date],
|
||||
title,
|
||||
[image],
|
||||
category,
|
||||
excerptHtml excerpt
|
||||
${fromSegment}
|
||||
ORDER BY ${opts.orderedBy}
|
||||
LIMIT ${opts.limit} OFFSET ${opts.offset}
|
||||
`
|
||||
)
|
||||
.all(cond) as {
|
||||
path: string
|
||||
date?: Date
|
||||
title: string
|
||||
image?: string
|
||||
category?: string
|
||||
excerpt: string
|
||||
}[]
|
||||
|
||||
return {
|
||||
result,
|
||||
count,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user