tldraw/apps/docs/utils/ContentDatabase.ts
Steve Ruiz 29044867dd
Add docs (#2470)
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]
2024-01-15 12:33:15 +00:00

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
}