/
storage.ts
148 lines (123 loc) · 4.7 KB
/
storage.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import { prefixStorage } from 'unstorage'
import { joinURL, withLeadingSlash } from 'ufo'
import { hash as ohash } from 'ohash'
import type { CompatibilityEvent } from 'h3'
import type { QueryBuilderParams, ParsedContent, QueryBuilder } from '../types'
import { createQuery } from '../query/query'
import { createPipelineFetcher } from '../query/match/pipeline'
import { parseContent } from './transformers'
import { getPreview, isPreview } from './preview'
// eslint-disable-next-line import/named
import { useRuntimeConfig, useStorage } from '#imports'
export const sourceStorage = prefixStorage(useStorage(), 'content:source')
export const cacheStorage = prefixStorage(useStorage(), 'cache:content')
export const cacheParsedStorage = prefixStorage(useStorage(), 'cache:content:parsed')
const isProduction = process.env.NODE_ENV === 'production'
const contentConfig = useRuntimeConfig().content
/**
* Content ignore patterns
*/
export const contentIgnores = contentConfig.ignores.map((p: any) =>
typeof p === 'string' ? new RegExp(`^${p}`) : p
)
/**
* Filter predicate for ignore patterns
*/
const contentIgnorePredicate = (key: string) =>
!key.startsWith('preview:') && !contentIgnores.some((prefix: RegExp) => key.split(':').some(k => prefix.test(k)))
export const getContentsIds = async (event: CompatibilityEvent, prefix?: string) => {
let keys = []
if (isProduction) {
keys = await cacheParsedStorage.getKeys(prefix)
}
// Later: handle preview mode, etc
if (keys.length === 0) {
keys = await sourceStorage.getKeys(prefix)
}
if (isPreview(event)) {
const { key } = getPreview(event)
const previewPrefix = `preview:${key}:${prefix || ''}`
const previewKeys = await sourceStorage.getKeys(previewPrefix)
if (previewKeys.length) {
const keysSet = new Set(keys)
await Promise.all(
previewKeys.map(async (key) => {
const meta = await sourceStorage.getMeta(key)
if (meta?.__deleted) {
keysSet.delete(key.substring(previewPrefix.length))
} else {
keysSet.add(key.substring(previewPrefix.length))
}
})
)
keys = Array.from(keysSet)
}
}
return keys.filter(contentIgnorePredicate)
}
export const getContentsList = async (event: CompatibilityEvent, prefix?: string) => {
const keys = await getContentsIds(event, prefix)
const contents = await Promise.all(keys.map(key => getContent(event, key)))
return contents
}
export const getContent = async (event: CompatibilityEvent, id: string): Promise<ParsedContent> => {
const contentId = id
// Handle ignored id
if (!contentIgnorePredicate(id)) {
return { _id: contentId, body: null }
}
if (isPreview(event)) {
const { key } = getPreview(event)
const previewId = `preview:${key}:${id}`
const draft = await sourceStorage.getItem(previewId)
if (draft) {
id = previewId
}
}
const cached: any = await cacheParsedStorage.getItem(id)
if (isProduction && cached) {
return cached.parsed
}
const meta = await sourceStorage.getMeta(id)
const hash = ohash({
meta,
// Add Content version to the hash, to revalidate the cache on content update
version: contentConfig.cacheVersion,
integerity: contentConfig.cacheIntegrity
})
if (cached?.hash === hash) {
return cached.parsed as ParsedContent
}
const body = await sourceStorage.getItem(id)
if (body === null) {
return { _id: contentId, body: null }
}
const parsed = await parseContent(contentId, body as string)
await cacheParsedStorage.setItem(id, { parsed, hash }).catch(() => {})
return parsed
}
/**
* Query contents
*/
export function serverQueryContent<T = ParsedContent>(event: CompatibilityEvent): QueryBuilder<T>;
export function serverQueryContent<T = ParsedContent>(event: CompatibilityEvent, params?: Partial<QueryBuilderParams>): QueryBuilder<T>;
export function serverQueryContent<T = ParsedContent>(event: CompatibilityEvent, path?: string, ...pathParts: string[]): QueryBuilder<T>;
export function serverQueryContent<T = ParsedContent> (event: CompatibilityEvent, path?: string | Partial<QueryBuilderParams>, ...pathParts: string[]) {
let params = (path || {}) as Partial<QueryBuilderParams>
if (typeof path === 'string') {
path = withLeadingSlash(joinURL(path, ...pathParts))
// escape regex special chars
path = path.replace(/[-[\]{}()*+.,^$\s]/g, '\\$&')
params = {
where: [{ _path: new RegExp(`^${path}`) }]
}
}
const pipelineFetcher = createPipelineFetcher<T>(
() => getContentsList(event) as unknown as Promise<T[]>
)
// Provide default sort order
if (!params.sort?.length) {
params.sort = [{ _file: 1, $numeric: true }]
}
return createQuery<T>(pipelineFetcher, params)
}