Comments (4)
First of all, Velite does not want to intrude into the user's runtime code. Its purpose is only the intermediate process from content to data layer. Velite will not interfere how users use the data generated by Velite.
e.g.
const authors = {
name: 'Author',
pattern: 'authors/index.yml',
schema: s
.object({
name: s.unique('authors'),
slug: slug,
avatar: s.image().optional()
})
}
const posts = {
name: 'Post',
pattern: 'posts/**/*.md',
schema: s
.object({
title: title,
slug: s.slug('post'),
description: paragraph.optional(),
author: s.string(), // <= author.name
content: s.markdown()
})
}
in your app
import { posts, authors } from '.velite'
const hydratedPosts = posts.map(item => ({ ... posts, author: authors.find(a => a.name === item.author) })
You can query the data in a functional way, and you can even let Velite help you write it to the database and then use SQL to query the data.
Secondly, from the perspective of the data source, dealing with related data is indeed a common requirement. I am also considering adding a s.reference()
schema to fulfill this need.
from velite.
here is my previous helper for access generated data
import { authors, categories, pages, plans, posts, tags } from '#/.velite'
import type { Author, Category, Page, Plan, Post, Tag } from '#/.velite'
type Taxonomy = {
authors: { [P in 'name' | 'slug' | 'email' | 'avatar' | 'bio' | 'permalink']: Author[P] }[]
categories: { [P in 'name' | 'slug' | 'permalink']: Category[P] }[]
tags: { [P in 'name' | 'slug' | 'permalink']: Tag[P] }[]
}
type Filter<T> = (value: T, index: number, array: T[]) => boolean
type Sorter<T> = (a: T, b: T) => number
const available = (item: { draft: boolean; private: boolean }) => process.env.NODE_ENV !== 'production' || (!item.draft && !item.private)
export const filters = {
none: (): boolean => true,
featured: (item: { featured: boolean }) => item.featured
}
export const sorters = {
dateAsc: <I extends { date: string }>(a: I, b: I): number => (a.date > b.date ? 1 : -1),
dateDesc: <I extends { date: string }>(a: I, b: I): number => (a.date > b.date ? -1 : 1),
nameAsc: <I extends { name: string }>(a: I, b: I): number => (a.name > b.name ? 1 : -1),
nameDesc: <I extends { name: string }>(a: I, b: I): number => (a.name > b.name ? -1 : 1),
priceAsc: <I extends { prices: { yearly: number } }>(a: I, b: I): number => (a.prices.yearly > b.prices.yearly ? 1 : -1),
priceDesc: <I extends { prices: { yearly: number } }>(a: I, b: I): number => (a.prices.yearly > b.prices.yearly ? -1 : 1),
countAsc: <I extends { count: { total: number } }>(a: I, b: I): number => (a.count.total > b.count.total ? 1 : -1),
countDesc: <I extends { count: { total: number } }>(a: I, b: I): number => (a.count.total > b.count.total ? -1 : 1),
titleAsc: <I extends { title: string }>(a: I, b: I): number => (a.title > b.title ? 1 : -1),
titleDesc: <I extends { title: string }>(a: I, b: I): number => (a.title > b.title ? -1 : 1)
}
const pick = <T extends object, K extends keyof T>(obj: T, keys?: K[]): { [P in K]: T[P] } => {
if (keys == null) return obj
return Object.fromEntries(keys.map(k => [k, obj[k]])) as { [P in K]: T[P] }
}
const include = async <I extends keyof Taxonomy = never>(data: { [P in keyof Taxonomy]: string[] }, includes?: I[]): Promise<{ [P in I]: Taxonomy[P] }> => {
if (includes == null) return {} as { [P in I]: Taxonomy[P] }
const entities = await Promise.all(
includes.map(async include => {
if (include === 'authors') {
return [
include,
(await getAuthors(['name', 'slug', 'email', 'avatar', 'bio', 'permalink'], i => data.authors.includes(i.name))) satisfies Taxonomy['authors']
]
} else if (include === 'categories') {
return [include, (await getCategories(['name', 'slug', 'permalink'], i => data.categories.includes(i.name))) satisfies Taxonomy['categories']]
} else if (include === 'tags') {
return [include, (await getTags(['name', 'slug', 'permalink'], i => data.tags.includes(i.name))) satisfies Taxonomy['tags']]
}
return [include, []]
})
)
return Object.fromEntries(entities)
}
export const getAuthors = async <F extends keyof Author>(
fields?: F[],
filter: Filter<Author> = filters.none,
limit: number = Infinity,
offset: number = 0
): Promise<{ [P in F]: Author[P] }[]> => {
return authors
.filter(filter)
.sort((a, b) => (a.count.total > b.count.total ? -1 : 1))
.slice(offset, offset + limit)
.map(author => pick(author, fields))
}
export const getAuthorsCount = async (filter: Filter<Author> = filters.none): Promise<number> => {
return authors.filter(filter).length
}
export const getAuthor = async <F extends keyof Author>(filter: Filter<Author>, fields?: F[]): Promise<{ [P in F]: Author[P] } | undefined> => {
const author = authors.find(filter)
return author && pick(author, fields)
}
export const getAuthorByName = async <F extends keyof Author>(name: string, fields?: F[]): Promise<{ [P in F]: Author[P] } | undefined> => {
return getAuthor(i => i.name === name, fields)
}
export const getAuthorBySlug = async <F extends keyof Author>(slug: string, fields?: F[]): Promise<{ [P in F]: Author[P] } | undefined> => {
return getAuthor(i => i.slug === slug, fields)
}
export const getCategories = async <F extends keyof Category>(
fields?: F[],
filter: Filter<Category> = filters.none,
limit: number = Infinity,
offset: number = 0
): Promise<{ [P in F]: Category[P] }[]> => {
return categories
.filter(filter)
.sort((a, b) => (a.count.total > b.count.total ? -1 : 1))
.slice(offset, offset + limit)
.map(author => pick(author, fields))
}
export const getCategoriesCount = async (filter: Filter<Category> = filters.none): Promise<number> => {
return categories.filter(filter).length
}
export const getCategory = async <F extends keyof Category>(filter: Filter<Category>, fields?: F[]): Promise<{ [P in F]: Category[P] } | undefined> => {
const category = categories.find(filter)
return category && pick(category, fields)
}
export const getCategoryByName = async <F extends keyof Category>(name: string, fields?: F[]): Promise<{ [P in F]: Category[P] } | undefined> => {
return getCategory(i => i.name === name, fields)
}
export const getCategoryBySlug = async <F extends keyof Category>(slug: string, fields?: F[]): Promise<{ [P in F]: Category[P] } | undefined> => {
return getCategory(i => i.slug === slug, fields)
}
export const getTags = async <F extends keyof Tag>(
fields?: F[],
filter: Filter<Tag> = filters.none,
limit: number = Infinity,
offset: number = 0
): Promise<{ [P in F]: Tag[P] }[]> => {
return tags
.filter(filter)
.sort((a, b) => (a.count.total > b.count.total ? -1 : 1))
.slice(offset, offset + limit)
.map(tag => pick(tag, fields))
}
export const getTagsCount = async (filter: Filter<Tag> = filters.none): Promise<number> => {
return tags.filter(filter).length
}
export const getTag = async <F extends keyof Tag>(filter: Filter<Tag>, fields?: F[]): Promise<{ [P in F]: Tag[P] } | undefined> => {
const tag = tags.find(filter)
return tag && pick(tag, fields)
}
export const getTagByName = async <F extends keyof Tag>(name: string, fields?: F[]): Promise<{ [P in F]: Tag[P] } | undefined> => {
return getTag(i => i.name === name, fields)
}
export const getTagBySlug = async <F extends keyof Tag>(slug: string, fields?: F[]): Promise<{ [P in F]: Tag[P] } | undefined> => {
return getTag(i => i.slug === slug, fields)
}
export const getPages = async <F extends keyof Page>(
fields?: F[],
filter: Filter<Page> = filters.none,
sorter: Sorter<Page> = sorters.titleAsc,
limit: number = Infinity,
offset: number = 0
): Promise<{ [P in F]: Page[P] }[]> => {
return pages
.filter(available)
.filter(filter)
.sort(sorter)
.slice(offset, offset + limit)
.map(page => pick(page, fields))
}
export const getPagesCount = async (filter: Filter<Page> = filters.none): Promise<number> => {
return pages.filter(available).filter(filter).length
}
export const getPage = async <F extends keyof Page>(filter: Filter<Page>, fields?: F[]): Promise<{ [P in F]: Page[P] } | undefined> => {
const page = pages.find(filter)
return page && pick(page, fields)
}
export const getPageBySlug = async <F extends keyof Page>(slug: string, fields?: F[]): Promise<{ [P in F]: Page[P] } | undefined> => {
return getPage(i => i.slug === slug, fields)
}
export const getPlans = async <F extends keyof Plan>(
fields?: F[],
filter: Filter<Plan> = filters.none,
sorter: Sorter<Plan> = sorters.priceAsc,
limit: number = Infinity,
offset: number = 0
): Promise<{ [P in F]: Plan[P] }[]> => {
return plans
.filter(available)
.filter(filter)
.sort(sorter)
.slice(offset, offset + limit)
.map(plan => pick(plan, fields))
}
export const getPlansCount = async (filter: Filter<Plan> = filters.none): Promise<number> => {
return plans.filter(available).filter(filter).length
}
export const getPlan = async <F extends keyof Plan>(filter: Filter<Plan>, fields?: F[]): Promise<{ [P in F]: Plan[P] } | undefined> => {
const plan = plans.find(filter)
return plan && pick(plan, fields)
}
export const getPlanBySlug = async <F extends keyof Plan>(slug: string, fields?: F[]): Promise<{ [P in F]: Plan[P] } | undefined> => {
return getPlan(i => i.slug === slug, fields)
}
export const getPosts = async <F extends keyof Omit<Post, I>, I extends keyof Taxonomy = never>(
fields?: F[],
includes?: I[],
filter: Filter<Post> = filters.none,
sorter: Sorter<Post> = sorters.dateDesc,
limit: number = Infinity,
offset: number = 0
): Promise<({ [P in F]: Post[P] } & { [P in I]: Taxonomy[P] })[]> => {
return Promise.all(
posts
.filter(available)
.filter(filter)
.sort(sorter)
.slice(offset, offset + limit)
.map(async post => ({ ...pick(post, fields), ...(await include(post, includes)) }))
)
}
export const getPostsCount = async (filter: Filter<Post> = filters.none): Promise<number> => {
return posts.filter(available).filter(filter).length
}
export const getPost = async <F extends keyof Omit<Post, I>, I extends keyof Taxonomy = never>(
filter: Filter<Post>,
fields?: F[],
includes?: I[]
): Promise<({ [P in F]: Post[P] } & { [P in I]: Taxonomy[P] }) | undefined> => {
const post = posts.find(filter)
return post && { ...pick(post, fields), ...(await include(post, includes)) }
}
export const getPostBySlug = async <F extends keyof Omit<Post, I>, I extends keyof Taxonomy = never>(
slug: string,
fields?: F[],
includes?: I[]
): Promise<({ [P in F]: Post[P] } & { [P in I]: Taxonomy[P] }) | undefined> => {
return getPost(i => i.slug === slug, fields, includes)
}
from velite.
I realize I wasn't quite clear in my initial message as I'm definitely most interested in the s.reference
schema you mentioned. My brief attempts at implementing it myself didn't quite work out as well as I wanted due to cyclical type references.
Thank you for the snippet though.
from velite.
I've got a somewhat working solution, it is still very far from perfect but at least it'll let me start working on my project without having to worry about messed up data integrity.
import { ZodType, logger } from "velite";
let CACHE: Map<string, string> | null = null;
const PREFIX_REFERENCE_KEY = "references:key";
const PREFIX_REFERENCE_VALUE = "references:value";
export default function createReference(
reference: string,
key: ZodType,
ref: ZodType,
): [ZodType, ZodType] {
return [
key.superRefine((value, { meta }) => {
const { cache } = meta.config;
cache.set(`${PREFIX_REFERENCE_KEY}:${reference}:${value}`, meta.path);
// cache is not accessible in prepare hook
if (CACHE === null) {
CACHE = cache;
}
}),
ref.superRefine((value, { meta }) => {
const { cache } = meta.config;
cache.set(`${PREFIX_REFERENCE_VALUE}:${reference}:${value}`, meta.path);
// cache is not accessible in prepare hook
if (CACHE === null) {
CACHE = cache;
}
}),
];
}
export function validateReferences() {
let error = false;
for (const [key, file] of CACHE?.entries() ?? []) {
if (!key.startsWith(PREFIX_REFERENCE_VALUE)) {
continue;
}
const [_0, _1, reference, value] = key.split(":");
if (!CACHE?.get(`${PREFIX_REFERENCE_KEY}:${reference}:${value}`)) {
logger.error(
`Referenced key ${value} does not exist for reference ${reference} (file: ${file})`,
);
error = true;
}
}
if (error) {
return false;
}
}
And it's then being used like this:
const [authorKey, authorRef] = createReference(
'authors',
s.slug('authors'), // type of the key
s.string(), // type of the reference
)
const authors = {
name: 'Author',
pattern: 'authors/index.yml',
schema: s
.object({
name: s.unique('authors'),
slug: authorKey,
avatar: s.image().optional(),
}),
};
const posts = {
name: 'Post',
pattern: 'posts/**/*.md',
schema: s
.object({
title: title,
slug: s.slug('post'),
description: paragraph.optional(),
author: authorRef,
content: s.markdown(),
}),
};
validateReferences
is then being called as the prepare
hook in the config:
export default defineConfig({
// [...collections]
prepare: validateReferences,
});
I'm still looking forward to an official solution on how to handle references!
from velite.
Related Issues (20)
- Path option for `s.image()` HOT 5
- Image Blur size is tool small to use, expose an option for setting it. HOT 2
- Build dependencies behind this expression are ignored and might cause incorrect cache invalidation.
- Working with 3000+ files. Out of Memoy HOT 1
- What is the purpose of the Recipes(especially typescript) in the documents? HOT 3
- Unhandled Runtime Error When using remarkCodeHike HOT 1
- Weird Page Freeze when Navigating via Table of Contents Links HOT 1
- Is it possible to use files stored inside github repo? HOT 4
- [feature request] handle contents failing schema validation, or autofixing HOT 8
- TOC breaking change/broken on latest release HOT 3
- The provided config in #37 resolves warning in dev mode but still present in deployment HOT 1
- Webpack cache warning HOT 2
- Property does not exist on type '{ props: { readonly components?: {} | undefined; }; }' .ts-plugin(2339) HOT 1
- `prepare`, `complete` events access to configuration `output` object HOT 3
- Taxonomy example dev server stops suddenly without error message HOT 1
- Property 'description', 'title', does not exist on type '{ slug: string; } & { slugAsParams: string; }'.ts(2339) HOT 3
- Support for additional metadata in mdx files HOT 2
- Unknown file extension ".css" HOT 9
- Support files with multiple documents (jsonc, yaml) HOT 4
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from velite.