Skip to content

Commit 12f41de

Browse files
committed
feat: add jsonld builder
1 parent d46e31c commit 12f41de

File tree

6 files changed

+1661
-478
lines changed

6 files changed

+1661
-478
lines changed
Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
/**
2+
* Utility functions for JSON-LD builder
3+
*
4+
* Provides configuration merging, validation, and transformation utilities
5+
* for the JSON-LD builder system.
6+
*/
7+
8+
import type { JsonLdEntity, JsonLdGraph, PropertyFilterConfig, PropertyFilterRule } from './jsonld-utils';
9+
import type {
10+
JsonLdConfig,
11+
JsonLdFilterOptions,
12+
PopulateConfig,
13+
PropertyFilterByIdRule,
14+
PropertyFilterByTypeRule,
15+
} from './types';
16+
17+
/**
18+
* Merge two configurations, with the second taking precedence
19+
*/
20+
export function mergeConfigs(base: JsonLdConfig, override: JsonLdConfig): JsonLdConfig {
21+
return {
22+
// Base graph: override takes precedence
23+
baseGraph: override.baseGraph ?? base.baseGraph,
24+
25+
// Filters: merge filter options
26+
filters: mergeFilterOptions(base.filters, override.filters),
27+
28+
// Subgraph roots: override takes precedence
29+
subgraphRoots: override.subgraphRoots ?? base.subgraphRoots,
30+
31+
// Property filters: combine arrays
32+
propertyFilters: combinePropertyFilters(base.propertyFilters, override.propertyFilters),
33+
34+
// Property filters by IDs: combine arrays
35+
propertyFiltersByIds: combineArrays(base.propertyFiltersByIds, override.propertyFiltersByIds),
36+
37+
// Property filters by types: combine arrays
38+
propertyFiltersByTypes: combineArrays(base.propertyFiltersByTypes, override.propertyFiltersByTypes),
39+
40+
// Additional entities: combine arrays
41+
additionalEntities: combineArrays(base.additionalEntities, override.additionalEntities),
42+
43+
// Pipes: combine arrays
44+
pipes: combineArrays(base.pipes, override.pipes),
45+
46+
// Populate config: override takes precedence
47+
populateConfig: override.populateConfig ?? base.populateConfig,
48+
};
49+
}
50+
51+
/**
52+
* Merge filter options, merging arrays and using override for single values
53+
*/
54+
function mergeFilterOptions(
55+
base?: JsonLdFilterOptions,
56+
override?: JsonLdFilterOptions,
57+
): JsonLdFilterOptions | undefined {
58+
if (!base && !override) return undefined;
59+
if (!base) return override;
60+
if (!override) return base;
61+
62+
return {
63+
// Merge arrays instead of replacing
64+
includeTypes: combineArrays(base.includeTypes, override.includeTypes),
65+
excludeTypes: combineArrays(base.excludeTypes, override.excludeTypes),
66+
includeIds: combineArrays(base.includeIds, override.includeIds),
67+
excludeIds: combineArrays(base.excludeIds, override.excludeIds),
68+
requiredProperties: combineArrays(base.requiredProperties, override.requiredProperties),
69+
excludeEntitiesWithProperties: combineArrays(
70+
base.excludeEntitiesWithProperties,
71+
override.excludeEntitiesWithProperties,
72+
),
73+
74+
// Single values - override takes precedence
75+
customFilter: override.customFilter ?? base.customFilter,
76+
maxEntities: override.maxEntities ?? base.maxEntities,
77+
};
78+
}
79+
80+
/**
81+
* Combine property filter configurations
82+
*/
83+
function combinePropertyFilters(
84+
base?: PropertyFilterConfig,
85+
override?: PropertyFilterConfig,
86+
): PropertyFilterConfig | undefined {
87+
if (!base && !override) return undefined;
88+
if (!base) return override;
89+
if (!override) return base;
90+
return [...base, ...override];
91+
}
92+
93+
/**
94+
* Combine arrays, handling undefined values
95+
*/
96+
function combineArrays<T>(base?: T[], override?: T[]): T[] | undefined {
97+
if (!base && !override) return undefined;
98+
if (!base) return override;
99+
if (!override) return base;
100+
return [...base, ...override];
101+
}
102+
103+
/**
104+
* Convert property filter by ID rules to PropertyFilterConfig
105+
*/
106+
export function convertPropertyFiltersByIds(rules: PropertyFilterByIdRule[]): PropertyFilterRule[] {
107+
return rules.flatMap((rule) =>
108+
rule.entityIds.map((entityId) => ({
109+
selector: { '@id': entityId },
110+
include: rule.include,
111+
exclude: rule.exclude,
112+
})),
113+
);
114+
}
115+
116+
/**
117+
* Convert property filter by type rules to PropertyFilterConfig
118+
*/
119+
export function convertPropertyFiltersByTypes(rules: PropertyFilterByTypeRule[]): PropertyFilterRule[] {
120+
return rules.flatMap((rule) =>
121+
rule.entityTypes.map((entityType) => ({
122+
selector: { '@type': entityType },
123+
include: rule.include,
124+
exclude: rule.exclude,
125+
})),
126+
);
127+
}
128+
129+
/**
130+
* Build complete property filter configuration from all sources
131+
*/
132+
export function buildPropertyFilterConfig(config: JsonLdConfig): PropertyFilterConfig | undefined {
133+
const filters: PropertyFilterRule[] = [];
134+
135+
// Add base property filters
136+
if (config.propertyFilters) {
137+
filters.push(...config.propertyFilters);
138+
}
139+
140+
// Add property filters by IDs
141+
if (config.propertyFiltersByIds) {
142+
filters.push(...convertPropertyFiltersByIds(config.propertyFiltersByIds));
143+
}
144+
145+
// Add property filters by types
146+
if (config.propertyFiltersByTypes) {
147+
filters.push(...convertPropertyFiltersByTypes(config.propertyFiltersByTypes));
148+
}
149+
150+
return filters.length > 0 ? filters : undefined;
151+
}
152+
153+
/**
154+
* Validate configuration for common issues
155+
*/
156+
export function validateConfig(config: JsonLdConfig): string[] {
157+
const errors: string[] = [];
158+
159+
// Check for conflicting include/exclude types
160+
if (config.filters?.includeTypes && config.filters?.excludeTypes) {
161+
const overlap = config.filters.includeTypes.filter((type) => config.filters!.excludeTypes!.includes(type));
162+
if (overlap.length > 0) {
163+
errors.push(`Conflicting include/exclude types: ${overlap.join(', ')}`);
164+
}
165+
}
166+
167+
// Check for conflicting include/exclude IDs
168+
if (config.filters?.includeIds && config.filters?.excludeIds) {
169+
const overlap = config.filters.includeIds.filter((id) => config.filters!.excludeIds!.includes(id));
170+
if (overlap.length > 0) {
171+
errors.push(`Conflicting include/exclude IDs: ${overlap.join(', ')}`);
172+
}
173+
}
174+
175+
// Check for invalid maxEntities
176+
if (config.filters?.maxEntities !== undefined && config.filters.maxEntities < 1) {
177+
errors.push('maxEntities must be greater than 0');
178+
}
179+
180+
return errors;
181+
}
182+
183+
/**
184+
* Apply populate configuration to entities in the graph
185+
*/
186+
export function applyPopulateConfig(graph: JsonLdGraph, populateConfig: PopulateConfig): JsonLdGraph {
187+
return graph.map((entity) => {
188+
const entityId = entity['@id'];
189+
const populateRules = populateConfig[entityId];
190+
191+
if (!populateRules || populateRules.length === 0) {
192+
return entity;
193+
}
194+
195+
// Create a copy of the entity to avoid mutation
196+
const populatedEntity = { ...entity };
197+
198+
// Apply each populate rule
199+
for (const rule of populateRules) {
200+
populatedEntity[rule.property] = rule.entities;
201+
}
202+
203+
return populatedEntity;
204+
});
205+
}
206+
207+
/**
208+
* Validate runtime configuration for critical errors only
209+
* This is more lenient than the general validateConfig to handle runtime overrides
210+
*/
211+
export function validateRuntimeConfig(config: JsonLdConfig): string[] {
212+
const errors: string[] = [];
213+
214+
// Check for baseGraph requirement
215+
if (!config.baseGraph) {
216+
errors.push('baseGraph is required');
217+
} else if (!Array.isArray(config.baseGraph)) {
218+
errors.push('baseGraph must be an array');
219+
}
220+
221+
// Only check for critical errors that would break processing
222+
if (config.filters?.maxEntities !== undefined && config.filters.maxEntities < 1) {
223+
errors.push('maxEntities must be greater than 0');
224+
}
225+
226+
return errors;
227+
}
228+
229+
/**
230+
* Type guard to check if an entity matches a type
231+
*/
232+
export function entityHasType(entity: JsonLdEntity, type: string): boolean {
233+
const entityType = entity['@type'];
234+
if (!entityType) return false;
235+
236+
if (Array.isArray(entityType)) {
237+
return entityType.includes(type);
238+
}
239+
240+
return entityType === type;
241+
}
242+
243+
/**
244+
* Type guard to check if an entity has any of the specified types
245+
*/
246+
export function entityHasAnyType(entity: JsonLdEntity, types: string[]): boolean {
247+
return types.some((type) => entityHasType(entity, type));
248+
}
249+
250+
/**
251+
* Get all types from an entity as an array
252+
*/
253+
export function getEntityTypes(entity: JsonLdEntity): string[] {
254+
const entityType = entity['@type'];
255+
if (!entityType) return [];
256+
257+
return Array.isArray(entityType) ? entityType : [entityType];
258+
}
259+
260+
/**
261+
* Apply filters to a JSON-LD graph (copied from head-manager/filters.ts for self-containment)
262+
*/
263+
export function filterJsonLdGraph(graph: JsonLdGraph, options: JsonLdFilterOptions): JsonLdGraph {
264+
let filtered = [...graph];
265+
266+
// Apply type inclusion filter
267+
if (options.includeTypes && options.includeTypes.length > 0) {
268+
filtered = filtered.filter((entity) => {
269+
const type = entity['@type'];
270+
if (!type) return false;
271+
272+
const types = Array.isArray(type) ? type : [type];
273+
return types.some((t) => options.includeTypes!.includes(t));
274+
});
275+
}
276+
277+
// Apply type exclusion filter
278+
if (options.excludeTypes && options.excludeTypes.length > 0) {
279+
filtered = filtered.filter((entity) => {
280+
const type = entity['@type'];
281+
if (!type) return true;
282+
283+
const types = Array.isArray(type) ? type : [type];
284+
return !types.some((t) => options.excludeTypes!.includes(t));
285+
});
286+
}
287+
288+
// Apply ID inclusion filter
289+
if (options.includeIds && options.includeIds.length > 0) {
290+
filtered = filtered.filter((entity) => entity['@id'] && options.includeIds!.includes(entity['@id']));
291+
}
292+
293+
// Apply ID exclusion filter
294+
if (options.excludeIds && options.excludeIds.length > 0) {
295+
filtered = filtered.filter((entity) => !entity['@id'] || !options.excludeIds!.includes(entity['@id']));
296+
}
297+
298+
// Apply required properties filter
299+
if (options.requiredProperties && options.requiredProperties.length > 0) {
300+
filtered = filtered.filter((entity) => options.requiredProperties!.every((prop) => prop in entity));
301+
}
302+
303+
// Apply excluded properties filter (handle both old and new property names)
304+
const excludeProps = options.excludeEntitiesWithProperties || (options as any).excludeProperties;
305+
if (excludeProps && excludeProps.length > 0) {
306+
filtered = filtered.filter((entity) => !excludeProps.some((prop: string) => prop in entity));
307+
}
308+
309+
// Apply custom filter
310+
if (options.customFilter) {
311+
filtered = filtered.filter(options.customFilter);
312+
}
313+
314+
// Apply max entities limit
315+
if (options.maxEntities && options.maxEntities > 0) {
316+
filtered = filtered.slice(0, options.maxEntities);
317+
}
318+
319+
return filtered;
320+
}

0 commit comments

Comments
 (0)