@@ -137,93 +137,177 @@ JsonApiToolkit supports rich querying via URL parameters. `jsonapi-react-tools`
137137
138138### Working with Relationships (Included Resources )
139139
140- When your API response includes related resources (using the ` include ` query parameter ), ` jsonapi-react-tools ` provides type safety for the `included` array. This requires defining a central registry of all your resource types.
140+ JsonApiToolkit responses separate primary resource data from related resources , which are returned in an ` included ` array . The primary resource ’s relationships contain only resource identifiers . To work with full resource objects and ensure type safety , you must first define your resource attributes and create a central type registry that maps resource type strings to these attribute interfaces.
141+
142+ #### Define Resource Attributes & Create a Type Registry
143+
144+ First , define interfaces for each resource ' s attributes:
145+
146+ ` ` ` ts
147+ export interface CompanyAttributes {
148+ companyName: string;
149+ companyCode: string;
150+ establishedAt: string;
151+ }
152+
153+ export interface LocationAttributes {
154+ address: string;
155+ city: string;
156+ country: string;
157+ }
158+
159+ export interface EmployeeAttributes {
160+ firstName: string;
161+ lastName: string;
162+ email: string;
163+ }
164+ ` ` `
141165
142- 1. ** Define Attribute Types for All Resources : ** Ensure you have attribute types defined not only for your primary resource ( e . g ., ` CompanyAttributes ` ) but also for any resources that might appear in the ` included ` array ( e . g ., ` LocationAttributes ` ).
166+ Then , create a registry that maps the resource type strings (as they are returned by your API) to your interfaces:
143167
144- ` ` ` ts
145- // types/Company.ts
146- export type CompanyAttributes = {
147- companyName: string;
148- companyCode: string;
149- // ... other fields
150- };
168+ ` ` ` ts
169+ import { JsonApiTypeRegistry } from "@intility/jsonapi-react-tools";
170+ import { CompanyAttributes } from "./Company";
171+ import { LocationAttributes } from "./Location";
172+ import { EmployeeAttributes } from "./Employee";
151173
152- // types/Location.ts
153- export type LocationAttributes = {
154- address: string;
155- city: string;
156- // ... other fields
157- };
158- ` ` `
174+ export interface AppTypeRegistry extends JsonApiTypeRegistry {
175+ companies: CompanyAttributes;
176+ locations: LocationAttributes;
177+ employees: EmployeeAttributes;
178+ }
179+ ` ` `
159180
160- 2. ** Create Your Application ' s Type Registry:** Implement the `JsonApiTypeRegistry` interface, mapping the exact `type` string returned by your API to the corresponding attribute interface .
181+ This registry is used throughout your application to ensure that all JSON : API responses are strongly typed .
161182
162- ` ` ` ts
163- // types/Registry.ts
164- import { JsonApiTypeRegistry } from "@intility/jsonapi-react-tools";
165- import { CompanyAttributes } from "./Company";
166- import { LocationAttributes } from "./Location";
167-
168- // Add all resource types that can appear in 'data' or 'included'
169- export interface AppTypeRegistry extends JsonApiTypeRegistry {
170- companies: CompanyAttributes; // 'companies' is the type string from the API
171- locations: LocationAttributes; // 'locations' is the type string from the API
172- }
173- ` ` `
183+ #### 2. Extracting Related Resources
174184
175- 3. ** Use the Registry in Your Data Fetching : ** Pass your ` AppTypeRegistry ` as the second generic argument to ` JsonApiCollectionDocument ` or ` JsonApiDocument ` .
185+ There are two cases for extracting related resources from a JSON : API response :
176186
177- ` ` ` tsx
178- import { useQuery } from "@tanstack/react-query";
179- import {
180- JsonApiCollectionDocument,
181- isResourceType, // Import the type predicate
182- buildJsonApiQueryString,
183- JsonApiQueryOptions,
184- } from "@intility/jsonapi-react-tools";
185- import { CompanyAttributes } from "~/types/Company.ts";
186- import { AppTypeRegistry } from "~/types/Registry.ts"; // Import your registry
187+ ** A . Single Resource Responses :**
188+ When fetching a single resource (using a ` JsonApiDocument ` ), all related resources are still available in the ` included ` array . In this case , you can use the built - in ` getIncludedOfType ` helper to filter the ` included ` array by resource type . For example :
187189
188- const queryOptions: JsonApiQueryOptions<CompanyAttributes> = {
189- include: ["locations"], // Request related locations
190- };
191- const queryString = buildJsonApiQueryString(queryOptions);
190+ ` ` ` tsx
191+ import { getIncludedOfType } from "@intility/jsonapi-react-tools";
192192
193- export const CompanyListWithLocations = () => {
194- // Use the registry in the useQuery type definition
195- const { data: response, error, isLoading } = useQuery<
196- JsonApiCollectionDocument<CompanyAttributes, AppTypeRegistry> // <--- Pass registry here
197- >({ queryKey: ["api", "companies", queryString] });
193+ export function CompanyDetail({ companyId }: { companyId: string }) {
194+ const { data: companyDocument, isLoading } = useCompany(companyId);
198195
199- if (isLoading) return <div>Loading...</div>;
200- if (error || !response) return <div>Error</div>;
196+ if (isLoading || !companyDocument || !companyDocument.data) {
197+ return <div>Loading company details...</div>;
198+ }
201199
202- // Process included data safely
203- response.included?.forEach((resource) => {
204- // Use the type predicate to check the type and narrow it down
205- if (isResourceType(resource, "locations")) {
206- // TypeScript now knows resource.attributes is LocationAttributes
207- console.log("Included Location City:", resource.attributes.city);
208- }
209- // Add checks for other included types if necessary
210- });
200+ // Use getIncludedOfType to extract related locations and employees
201+ const locations = getIncludedOfType(companyDocument, "locations");
202+ const employees = getIncludedOfType(companyDocument, "employees");
211203
212- return (
204+ return (
205+ <div>
206+ <h2>{companyDocument.data.attributes.companyName}</h2>
207+ <p>Code: {companyDocument.data.attributes.companyCode}</p>
208+ <p>
209+ Established:{" "}
210+ <FormatDate date={companyDocument.data.attributes.establishedAt} />
211+ </p>
212+ <section>
213+ <h3>Locations</h3>
213214 <ul>
214- {response.data. map((company ) => (
215- <li key={company .id}>
216- {company .attributes.companyName }
217- {/* You can now safely look up and display related data */ }
215+ {locations. map((loc ) => (
216+ <li key={loc .id}>
217+ {loc .attributes.address}, {loc.attributes.city},{" " }
218+ {loc.attributes.country }
218219 </li>
219220 ))}
220221 </ul>
221- );
222- };
223- ` ` `
222+ </section>
223+ <section>
224+ <h3>Employees</h3>
225+ <ul>
226+ {employees.map((emp) => (
227+ <li key={emp.id}>
228+ {emp.attributes.firstName} {emp.attributes.lastName} -{" "}
229+ {emp.attributes.email}
230+ </li>
231+ ))}
232+ </ul>
233+ </section>
234+ </div>
235+ );
236+ }
237+ ` ` `
224238
225- By defining the ` AppTypeRegistry ` and using the ` isResourceType ` type predicate , you gain full type safety when working with related resources included in your API responses.
239+ In this example , the company ’s related locations and employees are retrieved directly from the ` included ` array using ` getIncludedOfType ` .
240+
241+ ** B . Collection Responses :**
242+ When dealing with collection responses (using a ` JsonApiCollectionDocument ` ), each primary resource defines its relationships with only resource identifiers . To resolve these identifiers into full resource objects , use the ` resolveRelationship ` helper . For example , in a component listing companies :
243+
244+ ` ` ` tsx
245+ import { useCompanies } from "~/hooks/useCompanies";
246+ import { Table } from "@intility/bifrost-react";
247+ import { resolveRelationship } from "@intility/jsonapi-react-tools";
248+
249+ export function CompanyList() {
250+ const { data: companiesDocument, isLoading } = useCompanies();
251+
252+ if (isLoading || !companiesDocument) {
253+ return <div>Loading companies...</div>;
254+ }
255+
256+ return (
257+ <Table>
258+ <Table.Header>
259+ <Table.Row>
260+ <Table.HeaderCell>Company Name</Table.HeaderCell>
261+ <Table.HeaderCell>Locations</Table.HeaderCell>
262+ <Table.HeaderCell>Employees</Table.HeaderCell>
263+ </Table.Row>
264+ </Table.Header>
265+ <Table.Body>
266+ {companiesDocument.data.map((company) => {
267+ // Resolve the 'locations' and 'employees' relationships for each company.
268+ // Note: The relationship names here (e.g., "locations") must match those
269+ // returned by your API.
270+ const companyLocations = company.relationships?.locations?.data
271+ ? resolveRelationship(
272+ company.relationships.locations.data,
273+ companiesDocument,
274+ "locations"
275+ )
276+ : [];
277+ const companyEmployees = company.relationships?.employees?.data
278+ ? resolveRelationship(
279+ company.relationships.employees.data,
280+ companiesDocument,
281+ "employees"
282+ )
283+ : [];
284+
285+ return (
286+ <Table.Row key={company.id}>
287+ <Table.Cell>{company.attributes.companyName}</Table.Cell>
288+ <Table.Cell>
289+ {companyLocations.map((loc) => loc.attributes.address).join(
290+ ", "
291+ )}
292+ </Table.Cell>
293+ <Table.Cell>
294+ {companyEmployees
295+ .map(
296+ (emp) =>
297+ ` $ {emp .attributes .firstName } $ {emp .attributes .lastName }`
298+ )
299+ .join(", ")}
300+ </Table.Cell>
301+ </Table.Row>
302+ );
303+ })}
304+ </Table.Body>
305+ </Table>
306+ );
307+ }
308+ ` ` `
226309
310+ Here , the ` resolveRelationship ` helper maps the relationship identifiers (from the primary company resource ) to the full location and employee objects from the collection ’s ` included ` array .
227311
228312### Further Information
229313
0 commit comments