11import { Document , Db } from 'mongodb'
2+ import { z } from 'zod'
3+ import { PageRefRegex } from './PageRefRegex'
4+ import { escapeRegExp } from 'lodash-es'
25
36const PageCollection = defineCollectionSchema < PageDoc > ( 'pages' )
47const currentCacheVersion = 'v3'
58
9+ export const PageDatabaseQuery = z . object ( {
10+ match : z
11+ . record ( z . union ( [ z . string ( ) , z . array ( z . string ( ) ) ] ) )
12+ . optional ( )
13+ . describe (
14+ 'Only return pages that have front-matter matching these properties.' ,
15+ ) ,
16+ prefix : z
17+ . string ( )
18+ . regex ( PageRefRegex )
19+ . regex ( / \/ $ / , 'Prefix must end with a slash' )
20+ . optional ( )
21+ . describe (
22+ 'Only return pages with this prefix. The prefix must end with a slash.' ,
23+ ) ,
24+ } )
25+ export type PageDatabaseQuery = z . infer < typeof PageDatabaseQuery >
26+ export interface PageDatabaseQueryResult {
27+ count : number
28+ results : PageDatabaseQueryResultItem [ ]
29+ explain ?: any
30+ }
31+ export interface PageDatabaseQueryResultItem {
32+ lastModified : Date | null
33+ pageRef : string
34+ frontMatter : Record < string , any >
35+ }
36+
637export interface ContentsgartenPageDatabase {
738 getCached ( pageRef : string ) : Promise < PageData | null >
839 save ( pageRef : string , input : PageDataInput ) : Promise < PageData >
940 getRecentlyChangedPages ( ) : Promise < PageListingItem [ ] >
41+ queryPages ( query : PageDatabaseQuery ) : Promise < PageDatabaseQueryResult >
42+ checkTypo ( normalized : string ) : Promise < string [ ] >
1043}
1144
1245export interface PageListingItem {
1346 pageRef : string
1447 lastModified : Date
1548}
49+ const explainResult = z . object ( {
50+ executionStats : z . object ( {
51+ executionTimeMillis : z . any ( ) ,
52+ totalKeysExamined : z . any ( ) ,
53+ totalDocsExamined : z . any ( ) ,
54+ } ) ,
55+ } )
1656
1757export class MongoDBPageDatabase implements ContentsgartenPageDatabase {
18- constructor ( private db : Db ) { }
58+ constructor ( private db : Db ) {
59+ this . createIndex ( ) . catch ( ( err ) => {
60+ console . error ( 'Failed to create index' , err )
61+ } )
62+ }
63+ async createIndex ( ) {
64+ const collection = PageCollection . of ( this . db )
65+ return Promise . all ( [
66+ collection . createIndex ( {
67+ cacheVersion : 1 ,
68+ lastModified : - 1 ,
69+ } ) ,
70+ collection . createIndex ( {
71+ cacheVersion : 1 ,
72+ 'aux.keyValues' : 1 ,
73+ lastModified : - 1 ,
74+ } ) ,
75+ collection . createIndex ( {
76+ cacheVersion : 1 ,
77+ 'aux.normalized' : 1 ,
78+ } ) ,
79+ ] )
80+ }
1981 async getCached ( pageRef : string ) : Promise < PageData | null > {
2082 const collection = PageCollection . of ( this . db )
2183 const doc = await collection . findOne ( {
@@ -46,6 +108,7 @@ export class MongoDBPageDatabase implements ContentsgartenPageDatabase {
46108 const docs = await collection
47109 . find ( {
48110 cacheVersion : currentCacheVersion ,
111+ data : { $ne : null } ,
49112 lastModified : { $ne : null } ,
50113 } )
51114 . sort ( { lastModified : - 1 } )
@@ -56,6 +119,72 @@ export class MongoDBPageDatabase implements ContentsgartenPageDatabase {
56119 lastModified : doc . lastModified ! ,
57120 } ) )
58121 }
122+ async queryPages ( input : PageDatabaseQuery ) : Promise < PageDatabaseQueryResult > {
123+ const collection = PageCollection . of ( this . db )
124+ const filter = compileQuery ( input )
125+ const cursor = collection
126+ . find ( filter )
127+ . project ( {
128+ _id : 1 ,
129+ lastModified : 1 ,
130+ 'aux.frontmatter' : 1 ,
131+ } )
132+ . limit ( 1000 )
133+ const results = ( await cursor . toArray ( ) ) as PageDoc [ ]
134+ return {
135+ count : results . length ,
136+ results : results . map ( ( doc ) => ( {
137+ pageRef : doc . _id ,
138+ lastModified : doc . lastModified ,
139+ frontMatter : doc . aux . frontmatter ,
140+ } ) ) ,
141+ explain : {
142+ filter : filter ,
143+ executionStats : explainResult . parse (
144+ await cursor . explain ( 'executionStats' ) ,
145+ ) ,
146+ } ,
147+ }
148+ }
149+ async checkTypo ( normalized : string ) : Promise < string [ ] > {
150+ const collection = PageCollection . of ( this . db )
151+ const cursor = collection
152+ . find ( {
153+ cacheVersion : currentCacheVersion ,
154+ 'aux.normalized' : normalized ,
155+ data : { $ne : null } ,
156+ } )
157+ . project ( { _id : 1 } )
158+ . limit ( 10 )
159+ const results = await cursor . toArray ( )
160+ return results . map ( ( doc ) => doc . _id )
161+ }
162+ }
163+
164+ function compileQuery ( input : PageDatabaseQuery ) : any {
165+ let ands : any [ ] = [
166+ {
167+ cacheVersion : currentCacheVersion ,
168+ data : { $ne : null } ,
169+ } ,
170+ ]
171+ if ( input . match ) {
172+ ands . push (
173+ ...Object . entries ( input . match ) . map ( ( [ key , value ] ) => {
174+ const f = ( v : string ) => `${ key } =${ v } `
175+ if ( Array . isArray ( value ) ) {
176+ return { 'aux.keyValues' : { $in : value . map ( f ) } }
177+ }
178+ return { 'aux.keyValues' : f ( value ) }
179+ } ) ,
180+ )
181+ }
182+ if ( input . prefix ) {
183+ ands . push ( {
184+ _id : { $regex : `^${ escapeRegExp ( input . prefix ) } ` } ,
185+ } )
186+ }
187+ return { $and : ands }
59188}
60189
61190export interface PageDoc {
@@ -75,6 +204,8 @@ export interface PageDocFile {
75204}
76205export interface PageAuxiliaryData {
77206 frontmatter : Record < string , any >
207+ normalized ?: string
208+ keyValues ?: string [ ]
78209}
79210
80211export type PageData = Pick <
0 commit comments