Skip to content

Commit

Permalink
feat: new reference() API that fixes type inference
Browse files Browse the repository at this point in the history
  • Loading branch information
bholmesdev committed Apr 14, 2023
1 parent dc2b586 commit b798f7c
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 34 deletions.
18 changes: 9 additions & 9 deletions examples/with-data/src/content/config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
import { defineCollection, defineDataCollection, z, reference } from 'astro:content';

const blog = defineCollection({
schema: z.object({
title: z.string(),
banner: reference('banners'),
author: reference('authors'),
}),
});
import { defineCollection, defineDataCollection, z } from 'astro:content';

const banners = defineDataCollection({
schema: ({ image }) =>
Expand All @@ -24,4 +16,12 @@ const authors = defineDataCollection({
}),
});

const blog = defineCollection({
schema: z.object({
title: z.string(),
banner: banners.reference(),
author: authors.reference(),
}),
});

export const collections = { blog, authors, banners };
3 changes: 3 additions & 0 deletions examples/with-data/src/data/authors/fred.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "Fred K. Schott"
}
17 changes: 10 additions & 7 deletions packages/astro/src/content/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,18 +247,21 @@ async function render({
}

export function createReference({
dataCollectionToEntryMap,
getCollectionName,
map,
}: {
dataCollectionToEntryMap: CollectionToEntryMap;
getCollectionName(): Promise<string>;
map: CollectionToEntryMap;
}) {
return function reference(collection: string) {
return function reference() {
return z.string().transform(async (entryId: string, ctx) => {
const collectionName = await getCollectionName();
const flattenedErrorPath = ctx.path.join('.');
const entries = dataCollectionToEntryMap[collection];
const entries = map[collectionName];
if (!entries) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `**${flattenedErrorPath}:** Reference to ${collection} invalid. Collection does not exist or is empty.`,
message: `**${flattenedErrorPath}:** Reference to ${collectionName} invalid. Collection does not exist or is empty.`,
});
return;
}
Expand All @@ -271,7 +274,7 @@ export function createReference({
);
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `**${flattenedErrorPath}**: Reference to ${collection} invalid. Expected ${entryKeys
message: `**${flattenedErrorPath}**: Reference to ${collectionName} invalid. Expected ${entryKeys
.map((c) => JSON.stringify(c))
.join(' | ')}. Received ${JSON.stringify(entryId)}.`,
});
Expand All @@ -295,7 +298,7 @@ export function createReference({
} else {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `**${flattenedErrorPath}:** Referenced entry ${collection}${entryId} is invalid.`,
message: `**${flattenedErrorPath}:** Referenced entry ${collectionName}${entryId} is invalid.`,
});
}
}
Expand Down
7 changes: 4 additions & 3 deletions packages/astro/src/content/template/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,20 +67,21 @@ declare module 'astro:content' {
type ContentCollectionConfig<S extends BaseSchema> = {
type: 'content';
schema?: S | ((context: SchemaContext) => S);
reference(): import('astro/zod').ZodEffects<S>;
};

type DataCollectionConfig<S extends BaseSchema> = {
type: 'data';
key?: string;
schema?: S | ((context: SchemaContext) => S);
reference(): import('astro/zod').ZodEffects<S>;
};

export function defineCollection<S extends BaseSchema>(
input: Omit<ContentCollectionConfig<S>, 'type'>
input: Omit<ContentCollectionConfig<S>, 'type' | 'reference'>
): ContentCollectionConfig<S>;

export function defineDataCollection<S extends BaseSchema>(
input: Omit<DataCollectionConfig<S>, 'type'>
input: Omit<DataCollectionConfig<S>, 'type' | 'reference'>
): DataCollectionConfig<S>;

type AllValuesOf<T> = T extends any ? T[keyof T] : never;
Expand Down
54 changes: 39 additions & 15 deletions packages/astro/src/content/template/virtual-mod.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,6 @@ import {

export { z } from 'astro/zod';

export function defineCollection(config) {
return Object.assign(config, { type: 'content' });
}

export function defineDataCollection(config) {
return Object.assign(config, { type: 'data' });
}

// TODO: Remove this when having this fallback is no longer relevant. 2.3? 3.0? - erika, 2023-04-04
export const image = () => {
throw new Error(
'Importing `image()` from `astro:content` is no longer supported. See https://docs.astro.build/en/guides/assets/#update-content-collections-schemas for our new import instructions.'
);
};

const contentDir = '@@CONTENT_DIR@@';
const dataDir = '@@DATA_DIR@@';

Expand Down Expand Up @@ -52,6 +37,45 @@ const collectionToRenderEntryMap = createCollectionToGlobResultMap({
dir: contentDir,
});

let referenceKeyIncr = 0;

/**
* @param {any} partialConfig
* @param {'content' | 'data'} type
*/
function baseDefineCollection(partialConfig, type) {
// We don't know the collection name since this is defined on the `collections` export.
// Generate a unique key for the collection that we can use for lookups.
const referenceKey = String(referenceKeyIncr++);
return {
...partialConfig,
type,
reference: createReference({
map: type === 'content' ? contentCollectionToEntryMap : dataCollectionToEntryMap,
async getCollectionName() {
const { default: map } = await import('@@COLLECTION_NAME_BY_REFERENCE_KEY@@');
return map[referenceKey];
},
}),
referenceKey,
};
}

export function defineCollection(config) {
return baseDefineCollection(config, 'content');
}

export function defineDataCollection(config) {
return baseDefineCollection(config, 'data');
}

// TODO: Remove this when having this fallback is no longer relevant. 2.3? 3.0? - erika, 2023-04-04
export const image = () => {
throw new Error(
'Importing `image()` from `astro:content` is no longer supported. See https://docs.astro.build/en/guides/assets/#update-content-collections-schemas for our new import instructions.'
);
};

export const getCollection = createGetCollection({
contentCollectionToEntryMap,
collectionToRenderEntryMap,
Expand Down
14 changes: 14 additions & 0 deletions packages/astro/src/content/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ export const collectionConfigParser = z.union([
z.object({
type: z.literal('content').optional().default('content'),
schema: z.any().optional(),
referenceKey: z.string(),
}),
z.object({
type: z.literal('data'),
schema: z.any().optional(),
referenceKey: z.string(),
}),
]);

Expand Down Expand Up @@ -337,6 +339,18 @@ export async function loadContentConfig({
}
const config = contentConfigParser.safeParse(unparsedConfig);
if (config.success) {
let collectionNameByReferenceKey: Record<string, string> = {};
for (const [collectionName, collection] of Object.entries(config.data.collections)) {
collectionNameByReferenceKey[collection.referenceKey] = collectionName;
}

// We need a way to map collection config references *back* to their collection name.
// This generates a JSON map we can import when querying.
await fs.promises.writeFile(
new URL('reference-map.json', contentPaths.cacheDir),
JSON.stringify(collectionNameByReferenceKey)
);

// Check that data collections are properly configured using `defineDataCollection()`.
const dataEntryExts = getDataEntryExts(settings);

Expand Down
4 changes: 4 additions & 0 deletions packages/astro/src/content/vite-plugin-content-virtual-mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export function astroContentVirtualModPlugin({

const virtualModContents = fsMod
.readFileSync(contentPaths.virtualModTemplate, 'utf-8')
.replace(
'@@COLLECTION_NAME_BY_REFERENCE_KEY@@',
new URL('reference-map.json', contentPaths.cacheDir).pathname
)
.replace('@@CONTENT_DIR@@', relContentDir)
.replace('@@DATA_DIR@@', relDataDir)
.replace('@@CONTENT_ENTRY_GLOB_PATH@@', `${relContentDir}**/*${getExtGlob(contentEntryExts)}`)
Expand Down

0 comments on commit b798f7c

Please sign in to comment.