Skip to content

Commit c729e93

Browse files
authored
fix: missing support for enrichOptions (#4189)
## Purpose Add support for `enrichOptions` parameter to the Content API, allowing users to control the depth level of reference enrichment when fetching content. ## Approach and changes - [x] added `enrichOptions` property with `enrichLevel` and `model` support to `GetContentOptions` type - [x] implemented `enrichOptions` query parameter handling for `/query` and `/content` endpoints ## Testing instructions 1. Create a `@builder.io/sdk` test script 2. Run `getAll()` method with `enrichOptions`: ``` await builder.getAll('some-model-with-nested-references', { noTraverse: false, enrich: true, enrichOptions: { enrichLevel: 3, model: { 'model1': { fields: 'id,data.field1,data.field3', }, 'model2': { fields: 'data.id,data.field1,data.field2,data.field6', }, 'model3': { fields: 'data.and,so,on' }, }, }, fields: 'data.rootField1,data.rootField2,data.rootField3', limit: 100, query: { 'data.field': 'some value', }, }) ``` 4. Confirm the underlying API call includes `enrich=true`, `enrichOptions.enrichLevel=3` and `enrichOptions.model.model1=id,data.field1,data.field3` query parameters
1 parent 9f092eb commit c729e93

File tree

3 files changed

+220
-2
lines changed

3 files changed

+220
-2
lines changed

.changeset/bright-chefs-collect.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
"@builder.io/sdk": minor
3+
"@builder.io/react": minor
4+
---
5+
6+
Feat: Add support for `enrichOptions` parameter to control reference enrichment depth and field selection when fetching content.
7+
8+
This feature allows you to:
9+
- Control the depth level of nested reference enrichment (up to 4 levels)
10+
- Selectively include/exclude fields for each referenced model type
11+
- Optimize API responses by fetching only the data you need
12+
13+
Example usage:
14+
15+
```typescript
16+
// Basic enrichment with depth control
17+
await builder.getAll('page', {
18+
enrich: true,
19+
enrichOptions: {
20+
enrichLevel: 2 // Fetch 2 levels of nested references
21+
}
22+
});
23+
24+
// Advanced: Selective field inclusion per model
25+
await builder.getAll('page', {
26+
enrich: true,
27+
enrichOptions: {
28+
enrichLevel: 3,
29+
model: {
30+
'product': {
31+
fields: 'id,name,price',
32+
omit: 'data.internalNotes'
33+
},
34+
'category': {
35+
fields: 'id,name'
36+
}
37+
}
38+
}
39+
});
40+
```

packages/core/src/builder.class.test.ts

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Builder, GetContentOptions } from './builder.class';
1+
import { Builder } from './builder.class';
22
import { BehaviorSubject } from './classes/observable.class';
33
import { BuilderContent } from './types/content';
44

@@ -1292,4 +1292,103 @@ describe('getAll', () => {
12921292
{ headers: { Authorization: `Bearer ${AUTH_TOKEN}` } }
12931293
);
12941294
});
1295+
1296+
test('hits query url with enrich=true when passed in options', async () => {
1297+
const expectedModel = 'page';
1298+
1299+
await builder.getAll(expectedModel, { enrich: true });
1300+
1301+
expect(builder['makeFetchApiCall']).toBeCalledTimes(1);
1302+
expect(builder['makeFetchApiCall']).toBeCalledWith(
1303+
expect.stringContaining('enrich=true'),
1304+
expect.anything()
1305+
);
1306+
});
1307+
1308+
test('hits query url with enrichOptions.enrichLevel when passed in options', async () => {
1309+
const expectedModel = 'page';
1310+
1311+
await builder.getAll(expectedModel, {
1312+
enrich: true,
1313+
enrichOptions: { enrichLevel: 2 },
1314+
});
1315+
1316+
expect(builder['makeFetchApiCall']).toBeCalledTimes(1);
1317+
expect(builder['makeFetchApiCall']).toBeCalledWith(expect.stringContaining('enrich=true'), {
1318+
headers: { Authorization: `Bearer ${AUTH_TOKEN}` },
1319+
});
1320+
expect(builder['makeFetchApiCall']).toBeCalledWith(
1321+
expect.stringContaining('enrichOptions.enrichLevel=2'),
1322+
{ headers: { Authorization: `Bearer ${AUTH_TOKEN}` } }
1323+
);
1324+
});
1325+
1326+
test('hits content url with enrich=true when apiEndpoint is content', async () => {
1327+
const expectedModel = 'page';
1328+
1329+
builder.apiEndpoint = 'content';
1330+
await builder.getAll(expectedModel, { enrich: true });
1331+
1332+
expect(builder['makeFetchApiCall']).toBeCalledTimes(1);
1333+
expect(builder['makeFetchApiCall']).toBeCalledWith(expect.stringContaining('enrich=true'), {
1334+
headers: { Authorization: `Bearer ${AUTH_TOKEN}` },
1335+
});
1336+
});
1337+
1338+
test('hits content url with enrichOptions.enrichLevel when apiEndpoint is content', async () => {
1339+
const expectedModel = 'page';
1340+
1341+
builder.apiEndpoint = 'content';
1342+
await builder.getAll(expectedModel, {
1343+
enrich: true,
1344+
enrichOptions: { enrichLevel: 3 },
1345+
});
1346+
1347+
expect(builder['makeFetchApiCall']).toBeCalledTimes(1);
1348+
expect(builder['makeFetchApiCall']).toBeCalledWith(expect.stringContaining('enrich=true'), {
1349+
headers: { Authorization: `Bearer ${AUTH_TOKEN}` },
1350+
});
1351+
expect(builder['makeFetchApiCall']).toBeCalledWith(
1352+
expect.stringContaining('enrichOptions.enrichLevel=3'),
1353+
{ headers: { Authorization: `Bearer ${AUTH_TOKEN}` } }
1354+
);
1355+
});
1356+
1357+
test('hits query url with enrichOptions.model when passed complex model options', async () => {
1358+
const expectedModel = 'page';
1359+
1360+
await builder.getAll(expectedModel, {
1361+
enrich: true,
1362+
enrichOptions: {
1363+
enrichLevel: 2,
1364+
model: {
1365+
product: {
1366+
fields: 'id,name,price',
1367+
omit: 'data.internalNotes',
1368+
},
1369+
category: {
1370+
fields: 'id,name',
1371+
},
1372+
},
1373+
},
1374+
});
1375+
1376+
expect(builder['makeFetchApiCall']).toBeCalledTimes(1);
1377+
expect(builder['makeFetchApiCall']).toBeCalledWith(
1378+
expect.stringContaining('enrichOptions.enrichLevel=2'),
1379+
{ headers: { Authorization: `Bearer ${AUTH_TOKEN}` } }
1380+
);
1381+
expect(builder['makeFetchApiCall']).toBeCalledWith(
1382+
expect.stringContaining('enrichOptions.model.product.fields=id%2Cname%2Cprice'),
1383+
{ headers: { Authorization: `Bearer ${AUTH_TOKEN}` } }
1384+
);
1385+
expect(builder['makeFetchApiCall']).toBeCalledWith(
1386+
expect.stringContaining('enrichOptions.model.product.omit=data.internalNotes'),
1387+
{ headers: { Authorization: `Bearer ${AUTH_TOKEN}` } }
1388+
);
1389+
expect(builder['makeFetchApiCall']).toBeCalledWith(
1390+
expect.stringContaining('enrichOptions.model.category.fields=id%2Cname'),
1391+
{ headers: { Authorization: `Bearer ${AUTH_TOKEN}` } }
1392+
);
1393+
});
12951394
});

packages/core/src/builder.class.ts

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { IncomingMessage, ServerResponse } from 'http';
33
import { nextTick } from './functions/next-tick.function';
44
import { QueryString } from './classes/query-string.class';
55
import { BehaviorSubject } from './classes/observable.class';
6-
import { getFetch, SimplifiedFetchOptions } from './functions/fetch.function';
6+
import { getFetch } from './functions/fetch.function';
77
import { assign } from './functions/assign.function';
88
import { throttle } from './functions/throttle.function';
99
import { Animator } from './classes/animator.class';
@@ -495,6 +495,50 @@ export type GetContentOptions = AllowEnrich & {
495495
* draft mode and un-archived. Default is false.
496496
*/
497497
includeUnpublished?: boolean;
498+
499+
/**
500+
* Options to configure how enrichment works.
501+
* @see {@link https://www.builder.io/c/docs/content-api#code-enrich-options-code}
502+
*/
503+
enrichOptions?: {
504+
/**
505+
* The depth level for enriching references. For example, an enrichLevel of 1
506+
* would return one additional nested model within the original response.
507+
* The maximum level is 4.
508+
*/
509+
enrichLevel?: number;
510+
511+
/**
512+
* Model-specific enrichment options. Allows selective field inclusion/exclusion
513+
* for each referenced model type.
514+
*
515+
* @example
516+
* ```typescript
517+
* enrichOptions: {
518+
* model: {
519+
* 'product': {
520+
* fields: 'id,name,price',
521+
* omit: 'data.internalNotes'
522+
* },
523+
* 'category': {
524+
* fields: 'id,name'
525+
* }
526+
* }
527+
* }
528+
* ```
529+
*/
530+
model?: {
531+
[modelName: string]: {
532+
/** Comma-separated list of fields to include */
533+
fields?: string;
534+
/** Comma-separated list of fields to omit */
535+
omit?: string;
536+
[key: string]: any;
537+
};
538+
};
539+
540+
[key: string]: any;
541+
};
498542
};
499543

500544
export type Class = {
@@ -2698,6 +2742,15 @@ export class Builder {
26982742
queryParams.includeRefs = true;
26992743
}
27002744

2745+
if (this.apiEndpoint === 'query') {
2746+
if ('enrich' in options && options.enrich !== undefined) {
2747+
queryParams.enrich = options.enrich;
2748+
}
2749+
if (options.enrichOptions) {
2750+
this.flattenEnrichOptions(options.enrichOptions, 'enrichOptions', queryParams);
2751+
}
2752+
}
2753+
27012754
const properties: (keyof GetContentOptions)[] = [
27022755
'prerender',
27032756
'extractCss',
@@ -2724,6 +2777,16 @@ export class Builder {
27242777
}
27252778
}
27262779
}
2780+
2781+
// Handle enrich and enrichOptions for content endpoint
2782+
if (this.apiEndpoint === 'content') {
2783+
if ('enrich' in options && options.enrich !== undefined) {
2784+
queryParams.enrich = options.enrich;
2785+
}
2786+
if (options.enrichOptions) {
2787+
this.flattenEnrichOptions(options.enrichOptions, 'enrichOptions', queryParams);
2788+
}
2789+
}
27272790
}
27282791
if (this.preview && this.previewingModel === queue?.[0]?.model) {
27292792
queryParams.preview = 'true';
@@ -2928,6 +2991,22 @@ export class Builder {
29282991
return Builder.isBrowser && setCookie(name, value, expires);
29292992
}
29302993

2994+
/**
2995+
* Recursively flattens enrichOptions object into dot-notation query parameters
2996+
* @private
2997+
*/
2998+
private flattenEnrichOptions(obj: any, prefix: string, result: Record<string, any>): void {
2999+
for (const [key, value] of Object.entries(obj)) {
3000+
const newKey = `${prefix}.${key}`;
3001+
3002+
if (value && typeof value === 'object' && !Array.isArray(value)) {
3003+
this.flattenEnrichOptions(value, newKey, result);
3004+
} else {
3005+
result[newKey] = value;
3006+
}
3007+
}
3008+
}
3009+
29313010
getContent(modelName: string, options: GetContentOptions = {}) {
29323011
if (!this.apiKey) {
29333012
throw new Error(

0 commit comments

Comments
 (0)