2024-01-15 12:33:15 +00:00
|
|
|
import { SearchResult } from '@/types/search-types'
|
|
|
|
import { getDb } from '@/utils/ContentDatabase'
|
2024-02-05 14:32:50 +00:00
|
|
|
import { SEARCH_RESULTS, searchBucket, sectionTypeBucket } from '@/utils/search-api'
|
use native structuredClone on node, cloudflare workers, and in tests (#3166)
Currently, we only use native `structuredClone` in the browser, falling
back to `JSON.parse(JSON.stringify(...))` elsewhere, despite Node
supporting `structuredClone` [since
v17](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone)
and Cloudflare Workers supporting it [since
2022](https://blog.cloudflare.com/standards-compliant-workers-api/).
This PR adjusts our shim to use the native `structuredClone` on all
platforms, if available.
Additionally, `jsdom` doesn't implement `structuredClone`, a bug [open
since 2022](https://github.com/jsdom/jsdom/issues/3363). This PR patches
`jsdom` environment in all packages/apps that use it for tests.
Also includes a driveby removal of `deepCopy`, a function that is
strictly inferior to `structuredClone`.
### Change Type
<!-- ❗ Please select a 'Scope' label ❗️ -->
- [x] `sdk` — Changes the tldraw SDK
- [x] `dotcom` — Changes the tldraw.com web app
- [ ] `docs` — Changes to the documentation, examples, or templates.
- [ ] `vs code` — Changes to the vscode plugin
- [ ] `internal` — Does not affect user-facing stuff
<!-- ❗ Please select a 'Type' label ❗️ -->
- [ ] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [x] `improvement` — Improving existing features
- [x] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [ ] `tools` — Changes to infrastructure, CI, internal scripts,
debugging tools, etc.
- [ ] `dunno` — I don't know
### Test Plan
1. A smoke test would be enough
- [ ] Unit Tests
- [x] End to end tests
2024-03-18 17:16:09 +00:00
|
|
|
import { structuredClone } from '@tldraw/utils'
|
2024-02-05 20:46:07 +00:00
|
|
|
import { NextRequest } from 'next/server'
|
2024-01-15 12:33:15 +00:00
|
|
|
|
|
|
|
type Data = {
|
|
|
|
results: {
|
|
|
|
articles: SearchResult[]
|
|
|
|
apiDocs: SearchResult[]
|
2024-02-05 14:32:50 +00:00
|
|
|
examples: SearchResult[]
|
2024-01-15 12:33:15 +00:00
|
|
|
}
|
|
|
|
status: 'success' | 'error' | 'no-query'
|
|
|
|
}
|
|
|
|
|
|
|
|
const BANNED_HEADINGS = ['new', 'constructor', 'properties', 'example', 'methods']
|
|
|
|
|
|
|
|
export async function GET(req: NextRequest) {
|
|
|
|
const { searchParams } = new URL(req.url)
|
|
|
|
const query = searchParams.get('q')?.toLowerCase()
|
|
|
|
|
|
|
|
if (!query) {
|
|
|
|
return new Response(
|
|
|
|
JSON.stringify({
|
2024-02-05 14:32:50 +00:00
|
|
|
results: structuredClone(SEARCH_RESULTS),
|
2024-01-15 12:33:15 +00:00
|
|
|
status: 'no-query',
|
|
|
|
}),
|
|
|
|
{
|
|
|
|
status: 400,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2024-02-05 14:32:50 +00:00
|
|
|
const results: Data['results'] = structuredClone(SEARCH_RESULTS)
|
2024-01-15 12:33:15 +00:00
|
|
|
const db = await getDb()
|
|
|
|
const searchForArticle = await db.db.prepare(
|
|
|
|
`
|
2024-02-06 09:49:31 +00:00
|
|
|
SELECT id, title, sectionId, categoryId, content
|
|
|
|
FROM ftsArticles
|
|
|
|
WHERE ftsArticles MATCH ?
|
|
|
|
ORDER BY bm25(ftsArticles, 1000.0)
|
2024-01-15 12:33:15 +00:00
|
|
|
`,
|
2024-02-06 09:49:31 +00:00
|
|
|
query
|
2024-01-15 12:33:15 +00:00
|
|
|
)
|
|
|
|
|
2024-02-06 09:49:31 +00:00
|
|
|
await searchForArticle.all().then(async (queryResults) => {
|
2024-01-15 12:33:15 +00:00
|
|
|
for (const article of queryResults) {
|
|
|
|
const section = await db.getSection(article.sectionId)
|
|
|
|
const category = await db.getCategory(article.categoryId)
|
|
|
|
const isUncategorized = category.id === section.id + '_ucg'
|
|
|
|
|
2024-02-05 14:32:50 +00:00
|
|
|
results[searchBucket(article.sectionId)].push({
|
2024-01-15 12:33:15 +00:00
|
|
|
id: article.id,
|
|
|
|
type: 'article',
|
|
|
|
subtitle: isUncategorized ? section.title : `${section.title} / ${category.title}`,
|
|
|
|
title: article.title,
|
2024-02-05 14:32:50 +00:00
|
|
|
sectionType: sectionTypeBucket(section.id),
|
2024-01-15 12:33:15 +00:00
|
|
|
url: isUncategorized
|
|
|
|
? `${section.id}/${article.id}`
|
|
|
|
: `${section.id}/${category.id}/${article.id}`,
|
2024-02-06 09:49:31 +00:00
|
|
|
score: 0,
|
2024-01-15 12:33:15 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
const searchForArticleHeadings = await db.db.prepare(
|
|
|
|
`
|
|
|
|
SELECT id, title, articleId, slug
|
2024-02-06 09:49:31 +00:00
|
|
|
FROM ftsHeadings
|
|
|
|
WHERE ftsHeadings MATCH ?
|
|
|
|
ORDER BY bm25(ftsHeadings, 1000.0)
|
2024-01-15 12:33:15 +00:00
|
|
|
`,
|
2024-02-06 09:49:31 +00:00
|
|
|
query
|
2024-01-15 12:33:15 +00:00
|
|
|
)
|
|
|
|
|
2024-02-06 09:49:31 +00:00
|
|
|
await searchForArticleHeadings.all().then(async (queryResults) => {
|
2024-01-15 12:33:15 +00:00
|
|
|
for (const heading of queryResults) {
|
|
|
|
if (BANNED_HEADINGS.some((h) => heading.slug.endsWith(h))) continue
|
|
|
|
|
2024-02-05 14:32:50 +00:00
|
|
|
const article = await db.getArticle(heading.articleId)
|
2024-01-15 12:33:15 +00:00
|
|
|
const section = await db.getSection(article.sectionId)
|
|
|
|
const category = await db.getCategory(article.categoryId)
|
|
|
|
const isUncategorized = category.id === section.id + '_ucg'
|
|
|
|
|
2024-02-05 14:32:50 +00:00
|
|
|
results[searchBucket(article.sectionId)].push({
|
2024-01-15 12:33:15 +00:00
|
|
|
id: article.id + '#' + heading.slug,
|
|
|
|
type: 'heading',
|
|
|
|
subtitle: isUncategorized ? section.title : `${section.title} / ${category.title}`,
|
2024-02-05 14:32:50 +00:00
|
|
|
sectionType: sectionTypeBucket(section.id),
|
2024-01-15 12:33:15 +00:00
|
|
|
title:
|
2024-01-30 14:19:25 +00:00
|
|
|
section.id === 'reference'
|
2024-01-15 12:33:15 +00:00
|
|
|
? article.title + '.' + heading.title
|
|
|
|
: article.title + ': ' + heading.title,
|
|
|
|
url: isUncategorized
|
|
|
|
? `${section.id}/${article.id}#${heading.slug}`
|
|
|
|
: `${section.id}/${category.id}/${article.id}#${heading.slug}`,
|
2024-02-06 09:49:31 +00:00
|
|
|
score: 0,
|
2024-01-15 12:33:15 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2024-02-06 09:49:31 +00:00
|
|
|
Object.keys(results).forEach((section: string) => {
|
|
|
|
results[section as keyof Data['results']] = results[section as keyof Data['results']].slice(
|
|
|
|
0,
|
|
|
|
20
|
|
|
|
)
|
|
|
|
})
|
2024-02-05 14:32:50 +00:00
|
|
|
|
2024-01-15 12:33:15 +00:00
|
|
|
results.articles.sort(
|
|
|
|
(a, b) => (b.type === 'heading' ? -1 : 1) - (a.type === 'heading' ? -1 : 1)
|
|
|
|
)
|
|
|
|
|
|
|
|
return new Response(JSON.stringify({ results, status: 'success' }), {
|
|
|
|
status: 200,
|
|
|
|
})
|
|
|
|
} catch (e: any) {
|
|
|
|
return new Response(
|
|
|
|
JSON.stringify({
|
2024-02-05 14:32:50 +00:00
|
|
|
results: structuredClone(SEARCH_RESULTS),
|
2024-01-15 12:33:15 +00:00
|
|
|
status: 'error',
|
|
|
|
error: e.message,
|
|
|
|
}),
|
|
|
|
{
|
|
|
|
status: 500,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|