11import { AstroError , AstroErrorData } from '../core/errors/index.js' ;
22import { prependForwardSlash } from '../core/path.js' ;
3-
3+ import { ZodIssueCode , string as zodString , type z } from 'zod' ;
44import {
55 createComponent ,
66 createHeadAndContent ,
@@ -9,7 +9,10 @@ import {
99 renderTemplate ,
1010 renderUniqueStylesheet ,
1111 unescapeHTML ,
12+ type AstroComponentFactory ,
1213} from '../runtime/server/index.js' ;
14+ import type { ContentLookupMap } from './utils.js' ;
15+ import type { MarkdownHeading } from '@astrojs/markdown-remark' ;
1316
1417type LazyImport = ( ) => Promise < any > ;
1518type GlobResult = Record < string , LazyImport > ;
@@ -37,14 +40,31 @@ export function createCollectionToGlobResultMap({
3740
3841const cacheEntriesByCollection = new Map < string , any [ ] > ( ) ;
3942export function createGetCollection ( {
40- collectionToEntryMap,
43+ contentCollectionToEntryMap,
44+ dataCollectionToEntryMap,
4145 getRenderEntryImport,
4246} : {
43- collectionToEntryMap : CollectionToEntryMap ;
47+ contentCollectionToEntryMap : CollectionToEntryMap ;
48+ dataCollectionToEntryMap : CollectionToEntryMap ;
4449 getRenderEntryImport : GetEntryImport ;
4550} ) {
4651 return async function getCollection ( collection : string , filter ?: ( entry : any ) => unknown ) {
47- const lazyImports = Object . values ( collectionToEntryMap [ collection ] ?? { } ) ;
52+ let type : 'content' | 'data' ;
53+ if ( collection in contentCollectionToEntryMap ) {
54+ type = 'content' ;
55+ } else if ( collection in dataCollectionToEntryMap ) {
56+ type = 'data' ;
57+ } else {
58+ throw new AstroError ( {
59+ ...AstroErrorData . CollectionDoesNotExistError ,
60+ message : AstroErrorData . CollectionDoesNotExistError . message ( collection ) ,
61+ } ) ;
62+ }
63+ const lazyImports = Object . values (
64+ type === 'content'
65+ ? contentCollectionToEntryMap [ collection ]
66+ : dataCollectionToEntryMap [ collection ]
67+ ) ;
4868 let entries : any [ ] = [ ] ;
4969 // Cache `getCollection()` calls in production only
5070 // prevents stale cache in development
@@ -54,20 +74,26 @@ export function createGetCollection({
5474 entries = await Promise . all (
5575 lazyImports . map ( async ( lazyImport ) => {
5676 const entry = await lazyImport ( ) ;
57- return {
58- id : entry . id ,
59- slug : entry . slug ,
60- body : entry . body ,
61- collection : entry . collection ,
62- data : entry . data ,
63- async render ( ) {
64- return render ( {
77+ return type === 'content'
78+ ? {
79+ id : entry . id ,
80+ slug : entry . slug ,
81+ body : entry . body ,
6582 collection : entry . collection ,
83+ data : entry . data ,
84+ async render ( ) {
85+ return render ( {
86+ collection : entry . collection ,
87+ id : entry . id ,
88+ renderEntryImport : await getRenderEntryImport ( collection , entry . slug ) ,
89+ } ) ;
90+ } ,
91+ }
92+ : {
6693 id : entry . id ,
67- renderEntryImport : await getRenderEntryImport ( collection , entry . slug ) ,
68- } ) ;
69- } ,
70- } ;
94+ collection : entry . collection ,
95+ data : entry . data ,
96+ } ;
7197 } )
7298 ) ;
7399 cacheEntriesByCollection . set ( collection , entries ) ;
@@ -110,6 +136,121 @@ export function createGetEntryBySlug({
110136 } ;
111137}
112138
139+ export function createGetDataEntryById ( {
140+ dataCollectionToEntryMap,
141+ } : {
142+ dataCollectionToEntryMap : CollectionToEntryMap ;
143+ } ) {
144+ return async function getDataEntryById ( collection : string , id : string ) {
145+ const lazyImport =
146+ dataCollectionToEntryMap [ collection ] ?. [ /*TODO: filePathToIdMap*/ id + '.json' ] ;
147+
148+ // TODO: AstroError
149+ if ( ! lazyImport ) throw new Error ( `Entry ${ collection } → ${ id } was not found.` ) ;
150+ const entry = await lazyImport ( ) ;
151+
152+ return {
153+ id : entry . id ,
154+ collection : entry . collection ,
155+ data : entry . data ,
156+ } ;
157+ } ;
158+ }
159+
160+ type ContentEntryResult = {
161+ id : string ;
162+ slug : string ;
163+ body : string ;
164+ collection : string ;
165+ data : Record < string , any > ;
166+ render ( ) : Promise < RenderResult > ;
167+ } ;
168+
169+ type DataEntryResult = {
170+ id : string ;
171+ collection : string ;
172+ data : Record < string , any > ;
173+ } ;
174+
175+ type EntryLookupObject = { collection : string ; id : string } | { collection : string ; slug : string } ;
176+
177+ export function createGetEntry ( {
178+ getEntryImport,
179+ getRenderEntryImport,
180+ } : {
181+ getEntryImport : GetEntryImport ;
182+ getRenderEntryImport : GetEntryImport ;
183+ } ) {
184+ return async function getEntry (
185+ // Can either pass collection and identifier as 2 positional args,
186+ // Or pass a single object with the collection and identifier as properties.
187+ // This means the first positional arg can have different shapes.
188+ collectionOrLookupObject : string | EntryLookupObject ,
189+ _lookupId ?: string
190+ ) : Promise < ContentEntryResult | DataEntryResult | undefined > {
191+ let collection : string , lookupId : string ;
192+ if ( typeof collectionOrLookupObject === 'string' ) {
193+ collection = collectionOrLookupObject ;
194+ if ( ! _lookupId )
195+ throw new AstroError ( {
196+ ...AstroErrorData . UnknownContentCollectionError ,
197+ message : '`getEntry()` requires an entry identifier as the second argument.' ,
198+ } ) ;
199+ lookupId = _lookupId ;
200+ } else {
201+ collection = collectionOrLookupObject . collection ;
202+ // Identifier could be `slug` for content entries, or `id` for data entries
203+ lookupId =
204+ 'id' in collectionOrLookupObject
205+ ? collectionOrLookupObject . id
206+ : collectionOrLookupObject . slug ;
207+ }
208+
209+ const entryImport = await getEntryImport ( collection , lookupId ) ;
210+ if ( typeof entryImport !== 'function' ) return undefined ;
211+
212+ const entry = await entryImport ( ) ;
213+
214+ if ( entry . _internal . type === 'content' ) {
215+ return {
216+ id : entry . id ,
217+ slug : entry . slug ,
218+ body : entry . body ,
219+ collection : entry . collection ,
220+ data : entry . data ,
221+ async render ( ) {
222+ return render ( {
223+ collection : entry . collection ,
224+ id : entry . id ,
225+ renderEntryImport : await getRenderEntryImport ( collection , lookupId ) ,
226+ } ) ;
227+ } ,
228+ } ;
229+ } else if ( entry . _internal . type === 'data' ) {
230+ return {
231+ id : entry . id ,
232+ collection : entry . collection ,
233+ data : entry . data ,
234+ } ;
235+ }
236+ return undefined ;
237+ } ;
238+ }
239+
240+ export function createGetEntries ( getEntry : ReturnType < typeof createGetEntry > ) {
241+ return async function getEntries (
242+ entries : { collection : string ; id : string } [ ] | { collection : string ; slug : string } [ ]
243+ ) {
244+ return Promise . all ( entries . map ( ( e ) => getEntry ( e ) ) ) ;
245+ } ;
246+ }
247+
248+ type RenderResult = {
249+ Content : AstroComponentFactory ;
250+ headings : MarkdownHeading [ ] ;
251+ remarkPluginFrontmatter : Record < string , any > ;
252+ } ;
253+
113254async function render ( {
114255 collection,
115256 id,
@@ -118,7 +259,7 @@ async function render({
118259 collection : string ;
119260 id : string ;
120261 renderEntryImport ?: LazyImport ;
121- } ) {
262+ } ) : Promise < RenderResult > {
122263 const UnexpectedRenderError = new AstroError ( {
123264 ...AstroErrorData . UnknownContentCollectionError ,
124265 message : `Unexpected error while rendering ${ String ( collection ) } → ${ String ( id ) } .` ,
@@ -186,3 +327,38 @@ async function render({
186327 remarkPluginFrontmatter : mod . frontmatter ?? { } ,
187328 } ;
188329}
330+
331+ export function createReference ( { lookupMap } : { lookupMap : ContentLookupMap } ) {
332+ return function reference ( collection : string ) {
333+ return zodString ( ) . transform ( ( lookupId : string , ctx ) => {
334+ const flattenedErrorPath = ctx . path . join ( '.' ) ;
335+ if ( ! lookupMap [ collection ] ) {
336+ ctx . addIssue ( {
337+ code : ZodIssueCode . custom ,
338+ message : `**${ flattenedErrorPath } :** Reference to ${ collection } invalid. Collection does not exist or is empty.` ,
339+ } ) ;
340+ return ;
341+ }
342+
343+ const { type, entries } = lookupMap [ collection ] ;
344+ const entry = entries [ lookupId ] ;
345+
346+ if ( ! entry ) {
347+ ctx . addIssue ( {
348+ code : ZodIssueCode . custom ,
349+ message : `**${ flattenedErrorPath } **: Reference to ${ collection } invalid. Expected ${ Object . keys (
350+ entries
351+ )
352+ . map ( ( c ) => JSON . stringify ( c ) )
353+ . join ( ' | ' ) } . Received ${ JSON . stringify ( lookupId ) } .`,
354+ } ) ;
355+ return ;
356+ }
357+ // Content is still identified by slugs, so map to a `slug` key for consistency.
358+ if ( type === 'content' ) {
359+ return { slug : lookupId , collection } ;
360+ }
361+ return { id : lookupId , collection } ;
362+ } ) ;
363+ } ;
364+ }
0 commit comments