29044867dd
This PR adds the docs app back into the tldraw monorepo. ## Deploying We'll want to update our deploy script to update the SOURCE_SHA to the newest release sha... and then deploy the docs pulling api.json files from that release. We _could_ update the docs on every push to main, but we don't have to unless something has changed. Right now there's no automated deployments from this repo. ## Side effects To make this one work, I needed to update the lock file. This might be ok (new year new lock file), and everything builds as expected, though we may want to spend some time with our scripts to be sure that things are all good. I also updated our prettier installation, which decided to add trailing commas to every generic type. Which is, I suppose, [correct behavior](https://github.com/prettier/prettier-vscode/issues/955)? But that caused diffs in every file, which is unfortunate. ### Change Type - [x] `internal` — Any other changes that don't affect the published package[^2]
269 lines
8.1 KiB
TypeScript
269 lines
8.1 KiB
TypeScript
import { connect } from '@/scripts/functions/connect'
|
|
import { Database } from 'sqlite'
|
|
import sqlite3 from 'sqlite3'
|
|
import {
|
|
Article,
|
|
ArticleHeadings,
|
|
ArticleLinks,
|
|
Category,
|
|
Section,
|
|
SidebarContentArticleLink,
|
|
SidebarContentCategoryLink,
|
|
SidebarContentLink,
|
|
SidebarContentList,
|
|
} from '../types/content-types'
|
|
import { assert } from './assert'
|
|
|
|
export class ContentDatabase {
|
|
constructor(public db: Database<sqlite3.Database, sqlite3.Statement>) {}
|
|
|
|
async getArticle(articleId: string): Promise<Article> {
|
|
const article = await this.db.get<Article>(
|
|
`SELECT *, NULL as content FROM articles WHERE articles.id = ?`,
|
|
articleId
|
|
)
|
|
assert(article, `Could not find a article with articleId ${articleId}`)
|
|
return article
|
|
}
|
|
|
|
async getSection(sectionId: string, opts = {} as { optional?: boolean }) {
|
|
const section = await this.db.get('SELECT * FROM sections WHERE id = ?', sectionId)
|
|
if (!opts.optional) assert(section, `Could not find a section with sectionId ${sectionId}`)
|
|
return section
|
|
}
|
|
|
|
async getCategory(categoryId: string, opts = {} as { optional?: boolean }) {
|
|
const category = await this.db.get('SELECT * FROM categories WHERE id = ?', categoryId)
|
|
if (!opts.optional) assert(category, `Could not find a category with categoryId ${categoryId}`)
|
|
return category
|
|
}
|
|
|
|
async getArticleHeadings(articleId: string): Promise<ArticleHeadings> {
|
|
const headings = await this.db.all<ArticleHeadings>(
|
|
`SELECT * FROM headings WHERE headings.articleId = ? ORDER BY idx ASC`,
|
|
articleId
|
|
)
|
|
assert(headings, `Could not find headings for an article with articleId ${articleId}`)
|
|
return headings
|
|
}
|
|
|
|
async getCategoriesForSection(sectionId: string, opts = {} as { optional?: boolean }) {
|
|
const categories = await this.db.all<Category[]>(
|
|
'SELECT * FROM categories WHERE sectionId = ?',
|
|
sectionId
|
|
)
|
|
if (!opts.optional) assert(categories, `Could not find categories for sectionId ${sectionId}`)
|
|
return categories
|
|
}
|
|
|
|
async getCategoryArticles(sectionId: string, categoryId: string) {
|
|
const articles = await this.db.all<Article[]>(
|
|
'SELECT id, title, sectionId, categoryId, path FROM articles WHERE sectionId = ? AND categoryId = ?',
|
|
sectionId,
|
|
categoryId
|
|
)
|
|
assert(articles, `Could not find articles for category with categoryId ${categoryId}`)
|
|
return articles
|
|
}
|
|
|
|
async getCategoryArticlesCount(sectionId: string, categoryId: string) {
|
|
const res = await this.db.get<{ count: number }>(
|
|
'SELECT COUNT(*) AS count FROM articles WHERE sectionId = ? AND categoryId = ?',
|
|
sectionId,
|
|
categoryId
|
|
)
|
|
|
|
assert(res, `Could not find count of articles for category with categoryId ${categoryId}`)
|
|
return res.count
|
|
}
|
|
|
|
async getArticleLinks(article: Article): Promise<ArticleLinks> {
|
|
// and the article with the same section but next sectionIndex
|
|
const { sectionIndex } = article
|
|
|
|
// the prev is the article with the same section but one less sectionIndex
|
|
let prev = await this.db.get<Article>(
|
|
`SELECT id, title, categoryId, sectionId, path FROM articles WHERE articles.sectionId = ? AND articles.sectionIndex = ?`,
|
|
article.sectionId,
|
|
sectionIndex - 1
|
|
)
|
|
|
|
// If there's no next, then get the LAST article from the prev section
|
|
if (!prev) {
|
|
const { idx } = await this.db.get(
|
|
`SELECT idx FROM sections WHERE sections.id = ?`,
|
|
article.sectionId
|
|
)
|
|
|
|
const prevSection = await this.db.get(
|
|
`SELECT id FROM sections WHERE sections.idx = ?`,
|
|
idx - 1
|
|
)
|
|
if (prevSection) {
|
|
const { id: prevSectionId } = prevSection
|
|
// get the article with the section id and the highest section index
|
|
prev = await this.db.get<Article>(
|
|
// here we only need certian info for the link
|
|
`SELECT id, title, categoryId, sectionId, path FROM articles WHERE articles.sectionId = ? ORDER BY articles.sectionIndex DESC LIMIT 1`,
|
|
prevSectionId
|
|
)
|
|
}
|
|
}
|
|
|
|
// the next is the article with the same section but next sectionIndex
|
|
let next = await this.db.get<Article>(
|
|
`SELECT id, title, categoryId, sectionId, path FROM articles WHERE articles.sectionId = ? AND articles.sectionIndex = ?`,
|
|
article.sectionId,
|
|
sectionIndex + 1
|
|
)
|
|
|
|
// If there's no next, then get the FIRST article from the next section
|
|
if (!next) {
|
|
const { idx } = await this.db.get(
|
|
`SELECT idx FROM sections WHERE sections.id = ?`,
|
|
article.sectionId
|
|
)
|
|
|
|
const nextSection = await this.db.get(
|
|
`SELECT id FROM sections WHERE sections.idx = ?`,
|
|
idx + 1
|
|
)
|
|
|
|
if (nextSection) {
|
|
const { id: nextSectionId } = nextSection
|
|
next = await this.db.get<Article>(
|
|
`SELECT id, title, categoryId, sectionId, path FROM articles WHERE articles.sectionId = ? ORDER BY articles.sectionIndex ASC LIMIT 1`,
|
|
nextSectionId
|
|
)
|
|
}
|
|
}
|
|
|
|
return { prev: prev ?? null, next: next ?? null }
|
|
}
|
|
|
|
private _sidebarContentLinks: SidebarContentLink[] | undefined
|
|
|
|
async getSidebarContentList({
|
|
sectionId,
|
|
categoryId,
|
|
articleId,
|
|
}: {
|
|
sectionId?: string
|
|
categoryId?: string
|
|
articleId?: string
|
|
}): Promise<SidebarContentList> {
|
|
let links: SidebarContentLink[]
|
|
|
|
if (this._sidebarContentLinks && process.env.NODE_ENV !== 'development') {
|
|
// Use the previously cached sidebar links
|
|
links = this._sidebarContentLinks
|
|
} else {
|
|
// Generate sidebar links and cache them
|
|
links = []
|
|
|
|
const sections = await this.db.all<Section[]>('SELECT * FROM sections ORDER BY idx ASC')
|
|
|
|
for (const section of sections) {
|
|
if (!section.path) continue
|
|
|
|
const children: SidebarContentLink[] = []
|
|
|
|
if (section.sidebar_behavior === 'show-title') {
|
|
links.push({
|
|
type: 'article',
|
|
title: section.title,
|
|
url: section.path,
|
|
articleId: section.id,
|
|
})
|
|
continue
|
|
}
|
|
|
|
// If the article is in the getting-started section
|
|
// ... we place it at the top level of the sidebar
|
|
// ... so let's simplify its URL to reflect that
|
|
|
|
const categoriesForSection = await this.db.all<Category[]>(
|
|
`SELECT * FROM categories WHERE categories.sectionId = ? ORDER BY sectionIndex ASC`,
|
|
section.id
|
|
)
|
|
|
|
const ucg: SidebarContentLink[] = []
|
|
|
|
for (const category of categoriesForSection) {
|
|
const articlesForCategory = await this.db.all<Article[]>(
|
|
`SELECT * FROM articles WHERE articles.categoryId = ? ORDER BY categoryIndex ASC`,
|
|
category.id
|
|
)
|
|
|
|
if (category.id === section.id + '_ucg') {
|
|
// Push uncategorized articles to the child of the section
|
|
for (const article of articlesForCategory) {
|
|
if (!article.path) continue
|
|
const sidebarArticleLink: SidebarContentArticleLink = {
|
|
type: 'article',
|
|
articleId: article.id,
|
|
title: article.title,
|
|
url: article.path,
|
|
}
|
|
|
|
ucg.push(sidebarArticleLink)
|
|
}
|
|
} else {
|
|
if (!category.path) continue
|
|
|
|
// Push a category together to the section's children
|
|
const sidebarCategoryLink: SidebarContentCategoryLink = {
|
|
type: 'category',
|
|
title: category.title,
|
|
url: category.path,
|
|
children: [],
|
|
}
|
|
|
|
children.push(sidebarCategoryLink)
|
|
|
|
for (const article of articlesForCategory) {
|
|
if (!article.path) continue
|
|
|
|
// Add the category's child articles to the category
|
|
const sidebarArticleLink: SidebarContentArticleLink = {
|
|
type: 'article' as const,
|
|
articleId: article.id,
|
|
title: article.title,
|
|
url: article.path,
|
|
}
|
|
|
|
sidebarCategoryLink.children.push(sidebarArticleLink)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add the uncategorized articles to the end of the section
|
|
children.push(...ucg)
|
|
|
|
// Push the section to the sidebar
|
|
links.push({ type: 'section', title: section.title, url: section.path, children })
|
|
|
|
// Cache the links structure for next time
|
|
this._sidebarContentLinks = links
|
|
}
|
|
}
|
|
|
|
return {
|
|
sectionId: sectionId ?? null,
|
|
categoryId: categoryId ?? null,
|
|
articleId: articleId ?? null,
|
|
links,
|
|
}
|
|
}
|
|
}
|
|
|
|
let contentDatabase: ContentDatabase | null = null
|
|
|
|
export async function getDb(opts = {} as { reset?: boolean }) {
|
|
if (!contentDatabase || opts.reset) {
|
|
const db = await connect(opts)
|
|
contentDatabase = new ContentDatabase(db)
|
|
}
|
|
|
|
return contentDatabase
|
|
}
|