@piqit/resolvers 0.0.2 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/file-markdown.d.ts +1 -1
- package/dist/file-markdown.d.ts.map +1 -1
- package/dist/file-markdown.js +2 -2
- package/dist/file-markdown.js.map +1 -1
- package/dist/path-pattern.d.ts +2 -1
- package/dist/path-pattern.d.ts.map +1 -1
- package/dist/path-pattern.js.map +1 -1
- package/dist/static.d.ts +1 -1
- package/dist/static.d.ts.map +1 -1
- package/package.json +9 -9
- package/src/edge.ts +0 -15
- package/src/file-markdown.ts +0 -352
- package/src/frontmatter.ts +0 -269
- package/src/index.ts +0 -65
- package/src/markdown.ts +0 -262
- package/src/path-pattern.ts +0 -226
- package/src/static.ts +0 -227
package/src/path-pattern.ts
DELETED
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Path Pattern Parsing and Matching
|
|
3
|
-
*
|
|
4
|
-
* Parses patterns like `{year}/{slug}.md` and provides:
|
|
5
|
-
* - Type-level extraction of parameters
|
|
6
|
-
* - Glob pattern generation
|
|
7
|
-
* - Path matching to extract params
|
|
8
|
-
* - Path building from params
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
// =============================================================================
|
|
12
|
-
// Type-level Parameter Extraction
|
|
13
|
-
// =============================================================================
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Extract params from a path pattern string at the type level.
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* type Params = ExtractParams<'{year}/{slug}.md'>
|
|
20
|
-
* // = { year: string } & { slug: string }
|
|
21
|
-
*/
|
|
22
|
-
export type ExtractParams<P extends string> = P extends `${string}{${infer Param}}${infer Rest}`
|
|
23
|
-
? { [K in Param]: string } & ExtractParams<Rest>
|
|
24
|
-
: {}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Simplify a type by flattening intersections.
|
|
28
|
-
* Converts { a: string } & { b: string } to { a: string; b: string }
|
|
29
|
-
*/
|
|
30
|
-
export type Simplify<T> = { [K in keyof T]: T[K] }
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Extract and simplify params from a path pattern.
|
|
34
|
-
*/
|
|
35
|
-
export type PathParams<P extends string> = Simplify<ExtractParams<P>>
|
|
36
|
-
|
|
37
|
-
// =============================================================================
|
|
38
|
-
// Compiled Pattern Interface
|
|
39
|
-
// =============================================================================
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* A compiled path pattern that can match paths and generate globs.
|
|
43
|
-
*/
|
|
44
|
-
export interface CompiledPattern {
|
|
45
|
-
/**
|
|
46
|
-
* The original pattern string.
|
|
47
|
-
*/
|
|
48
|
-
pattern: string
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* The extracted parameter names.
|
|
52
|
-
*/
|
|
53
|
-
paramNames: string[]
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Generate a glob pattern, optionally constraining specific params.
|
|
57
|
-
*
|
|
58
|
-
* @param constraints - Optional param values to use instead of wildcards
|
|
59
|
-
* @returns A glob pattern string
|
|
60
|
-
*
|
|
61
|
-
* @example
|
|
62
|
-
* pattern.toGlob() // '** / *.md' (with {year}/{slug}.md)
|
|
63
|
-
* pattern.toGlob({ year: '2024' }) // '2024/*.md'
|
|
64
|
-
*/
|
|
65
|
-
toGlob(constraints?: Record<string, unknown>): string
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Match a path against this pattern and extract params.
|
|
69
|
-
*
|
|
70
|
-
* @param path - The file path to match (relative to base)
|
|
71
|
-
* @returns The extracted params, or null if no match
|
|
72
|
-
*
|
|
73
|
-
* @example
|
|
74
|
-
* pattern.match('2024/hello-world.md') // { year: '2024', slug: 'hello-world' }
|
|
75
|
-
* pattern.match('invalid') // null
|
|
76
|
-
*/
|
|
77
|
-
match(path: string): Record<string, string> | null
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Build a path from params.
|
|
81
|
-
*
|
|
82
|
-
* @param params - The param values
|
|
83
|
-
* @returns The constructed path
|
|
84
|
-
*
|
|
85
|
-
* @example
|
|
86
|
-
* pattern.build({ year: '2024', slug: 'hello' }) // '2024/hello.md'
|
|
87
|
-
*/
|
|
88
|
-
build(params: Record<string, string>): string
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// =============================================================================
|
|
92
|
-
// Pattern Parsing
|
|
93
|
-
// =============================================================================
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Regex to match parameter placeholders in patterns.
|
|
97
|
-
* Matches {paramName} where paramName is alphanumeric + underscores.
|
|
98
|
-
*/
|
|
99
|
-
const PARAM_REGEX = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Characters that need escaping in regex.
|
|
103
|
-
*/
|
|
104
|
-
const REGEX_ESCAPE = /[.*+?^${}()|[\]\\]/g
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Compile a path pattern string into a usable pattern object.
|
|
108
|
-
*
|
|
109
|
-
* @param pattern - The pattern string like '{year}/{slug}.md'
|
|
110
|
-
* @returns A CompiledPattern instance
|
|
111
|
-
*
|
|
112
|
-
* @example
|
|
113
|
-
* const pattern = compilePattern('{year}/{slug}.md')
|
|
114
|
-
* pattern.toGlob() // '** / *.md'
|
|
115
|
-
* pattern.match('2024/hello.md') // { year: '2024', slug: 'hello' }
|
|
116
|
-
*/
|
|
117
|
-
export function compilePattern(pattern: string): CompiledPattern {
|
|
118
|
-
// Extract all parameter names in order
|
|
119
|
-
const paramNames: string[] = []
|
|
120
|
-
let match: RegExpExecArray | null
|
|
121
|
-
const regex = new RegExp(PARAM_REGEX.source, "g")
|
|
122
|
-
while ((match = regex.exec(pattern)) !== null) {
|
|
123
|
-
paramNames.push(match[1])
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Build the match regex by replacing {param} with capturing groups
|
|
127
|
-
// Each param can match anything except path separators
|
|
128
|
-
const regexPattern = pattern
|
|
129
|
-
.replace(REGEX_ESCAPE, (char) => {
|
|
130
|
-
// Don't escape our param placeholders - we'll handle them separately
|
|
131
|
-
if (char === "{" || char === "}") return char
|
|
132
|
-
return "\\" + char
|
|
133
|
-
})
|
|
134
|
-
.replace(PARAM_REGEX, "([^/]+)")
|
|
135
|
-
|
|
136
|
-
const matchRegex = new RegExp(`^${regexPattern}$`)
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
pattern,
|
|
140
|
-
paramNames,
|
|
141
|
-
|
|
142
|
-
toGlob(constraints?: Record<string, unknown>): string {
|
|
143
|
-
let glob = pattern
|
|
144
|
-
|
|
145
|
-
// Replace each param with either its constrained value or a wildcard
|
|
146
|
-
for (const name of paramNames) {
|
|
147
|
-
const value = constraints?.[name]
|
|
148
|
-
if (value !== undefined && value !== null) {
|
|
149
|
-
// Use the constrained value
|
|
150
|
-
glob = glob.replace(`{${name}}`, String(value))
|
|
151
|
-
} else {
|
|
152
|
-
// Use a wildcard - * matches anything except /
|
|
153
|
-
glob = glob.replace(`{${name}}`, "*")
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return glob
|
|
158
|
-
},
|
|
159
|
-
|
|
160
|
-
match(path: string): Record<string, string> | null {
|
|
161
|
-
const result = matchRegex.exec(path)
|
|
162
|
-
if (!result) return null
|
|
163
|
-
|
|
164
|
-
const params: Record<string, string> = {}
|
|
165
|
-
for (let i = 0; i < paramNames.length; i++) {
|
|
166
|
-
params[paramNames[i]] = result[i + 1]
|
|
167
|
-
}
|
|
168
|
-
return params
|
|
169
|
-
},
|
|
170
|
-
|
|
171
|
-
build(params: Record<string, string>): string {
|
|
172
|
-
let result = pattern
|
|
173
|
-
for (const name of paramNames) {
|
|
174
|
-
const value = params[name]
|
|
175
|
-
if (value === undefined) {
|
|
176
|
-
throw new Error(`Missing required param: ${name}`)
|
|
177
|
-
}
|
|
178
|
-
result = result.replace(`{${name}}`, value)
|
|
179
|
-
}
|
|
180
|
-
return result
|
|
181
|
-
},
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// =============================================================================
|
|
186
|
-
// Schema for Path Params
|
|
187
|
-
// =============================================================================
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Create a StandardSchema for path params extracted from a pattern.
|
|
191
|
-
* This is used by the resolver to expose scanParams to piq core.
|
|
192
|
-
*
|
|
193
|
-
* @param pattern - The compiled pattern
|
|
194
|
-
* @returns A StandardSchema that validates param objects
|
|
195
|
-
*/
|
|
196
|
-
export function createParamsSchema(
|
|
197
|
-
pattern: CompiledPattern
|
|
198
|
-
): import("piqit").StandardSchema<Record<string, string>> {
|
|
199
|
-
return {
|
|
200
|
-
"~standard": {
|
|
201
|
-
version: 1,
|
|
202
|
-
vendor: "piqit/resolvers",
|
|
203
|
-
validate(value: unknown) {
|
|
204
|
-
if (value === null || typeof value !== "object") {
|
|
205
|
-
return {
|
|
206
|
-
issues: [{ message: "Expected object" }],
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const obj = value as Record<string, unknown>
|
|
211
|
-
|
|
212
|
-
// All params should be strings if present
|
|
213
|
-
for (const name of pattern.paramNames) {
|
|
214
|
-
const val = obj[name]
|
|
215
|
-
if (val !== undefined && typeof val !== "string") {
|
|
216
|
-
return {
|
|
217
|
-
issues: [{ message: `Param ${name} must be a string`, path: [name] }],
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return { value: obj as Record<string, string> }
|
|
223
|
-
},
|
|
224
|
-
},
|
|
225
|
-
}
|
|
226
|
-
}
|
package/src/static.ts
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Static Content Resolver
|
|
3
|
-
*
|
|
4
|
-
* A resolver for querying pre-compiled content in edge environments
|
|
5
|
-
* like Cloudflare Workers where filesystem access is not available.
|
|
6
|
-
*
|
|
7
|
-
* Content is compiled at build time and bundled as a static module.
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* // 1. Build script compiles content:
|
|
11
|
-
* // build-content.ts
|
|
12
|
-
* import { fileMarkdown } from "@piqit/resolvers";
|
|
13
|
-
* const posts = fileMarkdown({ ... });
|
|
14
|
-
* const allPosts = await posts.resolve({ scan: {}, filter: {}, select: ["params.*", "frontmatter.*", "body.*"] });
|
|
15
|
-
* await Bun.write("src/generated/content.ts", `export const posts = ${JSON.stringify(allPosts)};`);
|
|
16
|
-
*
|
|
17
|
-
* // 2. Worker imports and uses static resolver:
|
|
18
|
-
* // worker.ts
|
|
19
|
-
* import { posts } from "./generated/content";
|
|
20
|
-
* import { staticContent } from "@piqit/resolvers";
|
|
21
|
-
* import { piq } from "piqit";
|
|
22
|
-
*
|
|
23
|
-
* const postsResolver = staticContent(posts);
|
|
24
|
-
*
|
|
25
|
-
* const results = await piq.from(postsResolver)
|
|
26
|
-
* .filter({ author: "John" })
|
|
27
|
-
* .select("params.slug", "frontmatter.title")
|
|
28
|
-
* .exec();
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
import type { Resolver, StandardSchema } from "piqit"
|
|
32
|
-
|
|
33
|
-
// =============================================================================
|
|
34
|
-
// Filter Helpers
|
|
35
|
-
// =============================================================================
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Get a nested value from an object using dot-path notation.
|
|
39
|
-
*/
|
|
40
|
-
function getByPath(obj: unknown, path: string): unknown {
|
|
41
|
-
const parts = path.split(".")
|
|
42
|
-
let current: unknown = obj
|
|
43
|
-
|
|
44
|
-
for (const part of parts) {
|
|
45
|
-
if (current == null || typeof current !== "object") {
|
|
46
|
-
return undefined
|
|
47
|
-
}
|
|
48
|
-
current = (current as Record<string, unknown>)[part]
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return current
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Default filter implementation - checks equality on frontmatter fields.
|
|
56
|
-
*/
|
|
57
|
-
function defaultFilter<T>(item: T, filter: Partial<Record<string, unknown>>): boolean {
|
|
58
|
-
const frontmatter = (item as Record<string, unknown>).frontmatter as Record<string, unknown> | undefined
|
|
59
|
-
|
|
60
|
-
if (!frontmatter) {
|
|
61
|
-
return Object.keys(filter).length === 0
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
for (const [key, value] of Object.entries(filter)) {
|
|
65
|
-
if (frontmatter[key] !== value) {
|
|
66
|
-
return false
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return true
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Check if scan constraints match params.
|
|
75
|
-
*/
|
|
76
|
-
function matchesScan<T>(item: T, scan: Partial<Record<string, unknown>>): boolean {
|
|
77
|
-
const params = (item as Record<string, unknown>).params as Record<string, unknown> | undefined
|
|
78
|
-
|
|
79
|
-
if (!params) {
|
|
80
|
-
return Object.keys(scan).length === 0
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
for (const [key, value] of Object.entries(scan)) {
|
|
84
|
-
if (value !== undefined && params[key] !== value) {
|
|
85
|
-
return false
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return true
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Select specific fields from an item based on select paths.
|
|
94
|
-
*/
|
|
95
|
-
function selectFields<T extends object>(item: T, selectPaths: string[]): Partial<T> {
|
|
96
|
-
const result: Record<string, unknown> = {}
|
|
97
|
-
|
|
98
|
-
for (const path of selectPaths) {
|
|
99
|
-
// Handle wildcards like "params.*"
|
|
100
|
-
if (path.endsWith(".*")) {
|
|
101
|
-
const namespace = path.slice(0, -2)
|
|
102
|
-
const nsValue = getByPath(item, namespace)
|
|
103
|
-
if (nsValue && typeof nsValue === "object") {
|
|
104
|
-
result[namespace] = { ...nsValue as object }
|
|
105
|
-
}
|
|
106
|
-
} else {
|
|
107
|
-
// Regular path like "params.slug" or "frontmatter.title"
|
|
108
|
-
const parts = path.split(".")
|
|
109
|
-
const namespace = parts[0]
|
|
110
|
-
|
|
111
|
-
// Ensure namespace exists in result
|
|
112
|
-
if (!result[namespace]) {
|
|
113
|
-
result[namespace] = {}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Set the value
|
|
117
|
-
const value = getByPath(item, path)
|
|
118
|
-
if (parts.length === 2) {
|
|
119
|
-
(result[namespace] as Record<string, unknown>)[parts[1]] = value
|
|
120
|
-
} else {
|
|
121
|
-
// Deeper path - just copy the value
|
|
122
|
-
result[namespace] = value
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return result as Partial<T>
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// =============================================================================
|
|
131
|
-
// Schema Factories
|
|
132
|
-
// =============================================================================
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Create a passthrough schema that accepts any value.
|
|
136
|
-
* Used for static content where validation happened at build time.
|
|
137
|
-
*/
|
|
138
|
-
function createPassthroughSchema<T>(): StandardSchema<T> {
|
|
139
|
-
return {
|
|
140
|
-
"~standard": {
|
|
141
|
-
version: 1,
|
|
142
|
-
vendor: "piqit/resolvers/static",
|
|
143
|
-
validate(value: unknown) {
|
|
144
|
-
return { value: value as T }
|
|
145
|
-
},
|
|
146
|
-
},
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// =============================================================================
|
|
151
|
-
// Resolver Factory
|
|
152
|
-
// =============================================================================
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Create a static content resolver from pre-compiled data.
|
|
156
|
-
*
|
|
157
|
-
* This resolver is designed for edge environments like Cloudflare Workers
|
|
158
|
-
* where filesystem access is not available. Content is compiled at build
|
|
159
|
-
* time and bundled as a static module.
|
|
160
|
-
*
|
|
161
|
-
* @param data - Array of pre-compiled content items
|
|
162
|
-
* @returns A resolver that queries the static data
|
|
163
|
-
*
|
|
164
|
-
* @example
|
|
165
|
-
* // In your worker:
|
|
166
|
-
* import { posts } from "./generated/content";
|
|
167
|
-
* import { staticContent } from "@piqit/resolvers";
|
|
168
|
-
* import { piq } from "piqit";
|
|
169
|
-
*
|
|
170
|
-
* const postsResolver = staticContent(posts);
|
|
171
|
-
*
|
|
172
|
-
* export default {
|
|
173
|
-
* async fetch(request: Request) {
|
|
174
|
-
* const results = await piq.from(postsResolver)
|
|
175
|
-
* .scan({ year: "2024" })
|
|
176
|
-
* .select("params.slug", "frontmatter.title")
|
|
177
|
-
* .exec();
|
|
178
|
-
*
|
|
179
|
-
* return Response.json(results);
|
|
180
|
-
* }
|
|
181
|
-
* };
|
|
182
|
-
*/
|
|
183
|
-
export function staticContent<T extends object>(
|
|
184
|
-
data: T[]
|
|
185
|
-
): Resolver<
|
|
186
|
-
StandardSchema<Partial<Record<string, unknown>>>,
|
|
187
|
-
StandardSchema<Partial<Record<string, unknown>>>,
|
|
188
|
-
StandardSchema<T>
|
|
189
|
-
> {
|
|
190
|
-
const scanSchema = createPassthroughSchema<Partial<Record<string, unknown>>>()
|
|
191
|
-
const filterSchema = createPassthroughSchema<Partial<Record<string, unknown>>>()
|
|
192
|
-
const resultSchema = createPassthroughSchema<T>()
|
|
193
|
-
|
|
194
|
-
return {
|
|
195
|
-
schema: {
|
|
196
|
-
scanParams: scanSchema,
|
|
197
|
-
filterParams: filterSchema,
|
|
198
|
-
result: resultSchema,
|
|
199
|
-
},
|
|
200
|
-
|
|
201
|
-
async resolve(spec): Promise<Partial<T>[]> {
|
|
202
|
-
let results = [...data]
|
|
203
|
-
|
|
204
|
-
// Apply scan constraints (filter by params)
|
|
205
|
-
if (spec.scan && Object.keys(spec.scan).length > 0) {
|
|
206
|
-
results = results.filter((item) => matchesScan(item, spec.scan!))
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Apply filter constraints (filter by frontmatter)
|
|
210
|
-
if (spec.filter && Object.keys(spec.filter).length > 0) {
|
|
211
|
-
results = results.filter((item) => defaultFilter(item, spec.filter!))
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Apply select to return only requested fields
|
|
215
|
-
if (spec.select && spec.select.length > 0) {
|
|
216
|
-
return results.map((item) => selectFields(item, spec.select))
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return results
|
|
220
|
-
},
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Alias for staticContent - provides a more descriptive name for the use case.
|
|
226
|
-
*/
|
|
227
|
-
export const staticResolver = staticContent
|