@zenstackhq/server 3.5.0-beta.4 → 3.5.0-beta.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/api.js CHANGED
@@ -1784,6 +1784,7 @@ var RestApiHandler = class {
1784
1784
  modelNameMapping;
1785
1785
  reverseModelNameMapping;
1786
1786
  externalIdMapping;
1787
+ nestedRoutes;
1787
1788
  constructor(options) {
1788
1789
  this.options = options;
1789
1790
  this.validateOptions(options);
@@ -1803,6 +1804,7 @@ var RestApiHandler = class {
1803
1804
  lowerCaseFirst3(k),
1804
1805
  v
1805
1806
  ]));
1807
+ this.nestedRoutes = options.nestedRoutes ?? false;
1806
1808
  this.urlPatternMap = this.buildUrlPatternMap(segmentCharset);
1807
1809
  this.buildTypeMap();
1808
1810
  this.buildSerializers();
@@ -1820,7 +1822,8 @@ var RestApiHandler = class {
1820
1822
  urlSegmentCharset: z2.string().min(1).optional(),
1821
1823
  modelNameMapping: z2.record(z2.string(), z2.string()).optional(),
1822
1824
  externalIdMapping: z2.record(z2.string(), z2.string()).optional(),
1823
- queryOptions: queryOptionsSchema.optional()
1825
+ queryOptions: queryOptionsSchema.optional(),
1826
+ nestedRoutes: z2.boolean().optional()
1824
1827
  });
1825
1828
  const parseResult = schema.safeParse(options);
1826
1829
  if (!parseResult.success) {
@@ -1845,6 +1848,12 @@ var RestApiHandler = class {
1845
1848
  ":type",
1846
1849
  ":id"
1847
1850
  ]), options),
1851
+ ["nestedSingle"]: new UrlPattern(buildPath([
1852
+ ":type",
1853
+ ":id",
1854
+ ":relationship",
1855
+ ":childId"
1856
+ ]), options),
1848
1857
  ["fetchRelationship"]: new UrlPattern(buildPath([
1849
1858
  ":type",
1850
1859
  ":id",
@@ -1864,6 +1873,81 @@ var RestApiHandler = class {
1864
1873
  mapModelName(modelName) {
1865
1874
  return this.modelNameMapping[modelName] ?? modelName;
1866
1875
  }
1876
+ /**
1877
+ * Resolves child model type and reverse relation from a parent relation name.
1878
+ * e.g. given parentType='user', parentRelation='posts', returns { childType:'post', reverseRelation:'author' }
1879
+ */
1880
+ resolveNestedRelation(parentType, parentRelation) {
1881
+ const parentInfo = this.getModelInfo(parentType);
1882
+ if (!parentInfo) return void 0;
1883
+ const field = this.schema.models[parentInfo.name]?.fields[parentRelation];
1884
+ if (!field?.relation) return void 0;
1885
+ const reverseRelation = field.relation.opposite;
1886
+ if (!reverseRelation) return void 0;
1887
+ return {
1888
+ childType: lowerCaseFirst3(field.type),
1889
+ reverseRelation,
1890
+ isCollection: !!field.array
1891
+ };
1892
+ }
1893
+ mergeFilters(left, right) {
1894
+ if (!left) {
1895
+ return right;
1896
+ }
1897
+ if (!right) {
1898
+ return left;
1899
+ }
1900
+ return {
1901
+ AND: [
1902
+ left,
1903
+ right
1904
+ ]
1905
+ };
1906
+ }
1907
+ /**
1908
+ * Builds a WHERE filter for the child model that constrains results to those belonging to the given parent.
1909
+ * @param parentType lowercased parent model name
1910
+ * @param parentId parent resource ID string
1911
+ * @param parentRelation relation field name on the parent model (e.g. 'posts')
1912
+ */
1913
+ buildNestedParentFilter(parentType, parentId, parentRelation) {
1914
+ const parentInfo = this.getModelInfo(parentType);
1915
+ if (!parentInfo) {
1916
+ return {
1917
+ filter: void 0,
1918
+ error: this.makeUnsupportedModelError(parentType)
1919
+ };
1920
+ }
1921
+ const resolved = this.resolveNestedRelation(parentType, parentRelation);
1922
+ if (!resolved) {
1923
+ return {
1924
+ filter: void 0,
1925
+ error: this.makeError("invalidPath", `invalid nested route: cannot resolve relation "${parentType}.${parentRelation}"`)
1926
+ };
1927
+ }
1928
+ const { reverseRelation } = resolved;
1929
+ const childInfo = this.getModelInfo(resolved.childType);
1930
+ if (!childInfo) {
1931
+ return {
1932
+ filter: void 0,
1933
+ error: this.makeUnsupportedModelError(resolved.childType)
1934
+ };
1935
+ }
1936
+ const reverseRelInfo = childInfo.relationships[reverseRelation];
1937
+ const relationFilter = reverseRelInfo?.isCollection ? {
1938
+ [reverseRelation]: {
1939
+ some: this.makeIdFilter(parentInfo.idFields, parentId, false)
1940
+ }
1941
+ } : {
1942
+ [reverseRelation]: {
1943
+ is: this.makeIdFilter(parentInfo.idFields, parentId, false)
1944
+ }
1945
+ };
1946
+ return {
1947
+ filter: relationFilter,
1948
+ error: void 0
1949
+ };
1950
+ }
1867
1951
  matchUrlPattern(path, routeType) {
1868
1952
  const pattern = this.urlPatternMap[routeType];
1869
1953
  if (!pattern) {
@@ -1911,6 +1995,10 @@ var RestApiHandler = class {
1911
1995
  if (match4) {
1912
1996
  return await this.processReadRelationship(client, match4.type, match4.id, match4.relationship, query);
1913
1997
  }
1998
+ match4 = this.matchUrlPattern(path, "nestedSingle");
1999
+ if (match4 && this.nestedRoutes && this.resolveNestedRelation(match4.type, match4.relationship)?.isCollection) {
2000
+ return await this.processNestedSingleRead(client, match4.type, match4.id, match4.relationship, match4.childId, query);
2001
+ }
1914
2002
  match4 = this.matchUrlPattern(path, "collection");
1915
2003
  if (match4) {
1916
2004
  return await this.processCollectionRead(client, match4.type, query);
@@ -1921,6 +2009,10 @@ var RestApiHandler = class {
1921
2009
  if (!requestBody) {
1922
2010
  return this.makeError("invalidPayload");
1923
2011
  }
2012
+ const nestedMatch = this.matchUrlPattern(path, "fetchRelationship");
2013
+ if (nestedMatch && this.nestedRoutes && this.resolveNestedRelation(nestedMatch.type, nestedMatch.relationship)?.isCollection) {
2014
+ return await this.processNestedCreate(client, nestedMatch.type, nestedMatch.id, nestedMatch.relationship, query, requestBody);
2015
+ }
1924
2016
  let match4 = this.matchUrlPattern(path, "collection");
1925
2017
  if (match4) {
1926
2018
  const body = requestBody;
@@ -1943,24 +2035,36 @@ var RestApiHandler = class {
1943
2035
  if (!requestBody) {
1944
2036
  return this.makeError("invalidPayload");
1945
2037
  }
1946
- let match4 = this.matchUrlPattern(path, "single");
2038
+ let match4 = this.matchUrlPattern(path, "relationship");
1947
2039
  if (match4) {
1948
- return await this.processUpdate(client, match4.type, match4.id, query, requestBody);
2040
+ return await this.processRelationshipCRUD(client, "update", match4.type, match4.id, match4.relationship, query, requestBody);
1949
2041
  }
1950
- match4 = this.matchUrlPattern(path, "relationship");
2042
+ const nestedToOnePatchMatch = this.matchUrlPattern(path, "fetchRelationship");
2043
+ if (nestedToOnePatchMatch && this.nestedRoutes && this.resolveNestedRelation(nestedToOnePatchMatch.type, nestedToOnePatchMatch.relationship) && !this.resolveNestedRelation(nestedToOnePatchMatch.type, nestedToOnePatchMatch.relationship)?.isCollection) {
2044
+ return await this.processNestedUpdate(client, nestedToOnePatchMatch.type, nestedToOnePatchMatch.id, nestedToOnePatchMatch.relationship, void 0, query, requestBody);
2045
+ }
2046
+ const nestedPatchMatch = this.matchUrlPattern(path, "nestedSingle");
2047
+ if (nestedPatchMatch && this.nestedRoutes && this.resolveNestedRelation(nestedPatchMatch.type, nestedPatchMatch.relationship)?.isCollection) {
2048
+ return await this.processNestedUpdate(client, nestedPatchMatch.type, nestedPatchMatch.id, nestedPatchMatch.relationship, nestedPatchMatch.childId, query, requestBody);
2049
+ }
2050
+ match4 = this.matchUrlPattern(path, "single");
1951
2051
  if (match4) {
1952
- return await this.processRelationshipCRUD(client, "update", match4.type, match4.id, match4.relationship, query, requestBody);
2052
+ return await this.processUpdate(client, match4.type, match4.id, query, requestBody);
1953
2053
  }
1954
2054
  return this.makeError("invalidPath");
1955
2055
  }
1956
2056
  case "DELETE": {
1957
- let match4 = this.matchUrlPattern(path, "single");
2057
+ let match4 = this.matchUrlPattern(path, "relationship");
1958
2058
  if (match4) {
1959
- return await this.processDelete(client, match4.type, match4.id);
2059
+ return await this.processRelationshipCRUD(client, "delete", match4.type, match4.id, match4.relationship, query, requestBody);
1960
2060
  }
1961
- match4 = this.matchUrlPattern(path, "relationship");
2061
+ const nestedDeleteMatch = this.matchUrlPattern(path, "nestedSingle");
2062
+ if (nestedDeleteMatch && this.nestedRoutes && this.resolveNestedRelation(nestedDeleteMatch.type, nestedDeleteMatch.relationship)?.isCollection) {
2063
+ return await this.processNestedDelete(client, nestedDeleteMatch.type, nestedDeleteMatch.id, nestedDeleteMatch.relationship, nestedDeleteMatch.childId);
2064
+ }
2065
+ match4 = this.matchUrlPattern(path, "single");
1962
2066
  if (match4) {
1963
- return await this.processRelationshipCRUD(client, "delete", match4.type, match4.id, match4.relationship, query, requestBody);
2067
+ return await this.processDelete(client, match4.type, match4.id);
1964
2068
  }
1965
2069
  return this.makeError("invalidPath");
1966
2070
  }
@@ -2047,20 +2151,23 @@ var RestApiHandler = class {
2047
2151
  log(this.log, "debug", () => `sending error response: ${safeJSONStringify(resp)}${err instanceof Error ? "\n" + err.stack : ""}`);
2048
2152
  return resp;
2049
2153
  }
2050
- async processSingleRead(client, type, resourceId, query) {
2051
- const typeInfo = this.getModelInfo(type);
2052
- if (!typeInfo) {
2053
- return this.makeUnsupportedModelError(type);
2054
- }
2055
- const args = {
2056
- where: this.makeIdFilter(typeInfo.idFields, resourceId)
2057
- };
2154
+ /**
2155
+ * Builds the ORM `args` object (include, select) shared by single-read operations.
2156
+ * Returns the args to pass to findUnique/findFirst and the resolved `include` list for serialization,
2157
+ * or an error response if query params are invalid.
2158
+ */
2159
+ buildSingleReadArgs(type, query) {
2160
+ const args = {};
2058
2161
  this.includeRelationshipIds(type, args, "include");
2059
2162
  let include;
2060
2163
  if (query?.["include"]) {
2061
2164
  const { select: select2, error: error2, allIncludes } = this.buildRelationSelect(type, query["include"], query);
2062
2165
  if (error2) {
2063
- return error2;
2166
+ return {
2167
+ args,
2168
+ include,
2169
+ error: error2
2170
+ };
2064
2171
  }
2065
2172
  if (select2) {
2066
2173
  args.include = {
@@ -2071,7 +2178,11 @@ var RestApiHandler = class {
2071
2178
  include = allIncludes;
2072
2179
  }
2073
2180
  const { select, error } = this.buildPartialSelect(type, query);
2074
- if (error) return error;
2181
+ if (error) return {
2182
+ args,
2183
+ include,
2184
+ error
2185
+ };
2075
2186
  if (select) {
2076
2187
  args.select = {
2077
2188
  ...select,
@@ -2085,6 +2196,19 @@ var RestApiHandler = class {
2085
2196
  args.include = void 0;
2086
2197
  }
2087
2198
  }
2199
+ return {
2200
+ args,
2201
+ include
2202
+ };
2203
+ }
2204
+ async processSingleRead(client, type, resourceId, query) {
2205
+ const typeInfo = this.getModelInfo(type);
2206
+ if (!typeInfo) {
2207
+ return this.makeUnsupportedModelError(type);
2208
+ }
2209
+ const { args, include, error } = this.buildSingleReadArgs(type, query);
2210
+ if (error) return error;
2211
+ args.where = this.makeIdFilter(typeInfo.idFields, resourceId);
2088
2212
  const entity = await client[type].findUnique(args);
2089
2213
  if (entity) {
2090
2214
  return {
@@ -2269,8 +2393,12 @@ var RestApiHandler = class {
2269
2393
  }
2270
2394
  if (limit === Infinity) {
2271
2395
  const entities = await client[type].findMany(args);
2396
+ const mappedType = this.mapModelName(type);
2272
2397
  const body = await this.serializeItems(type, entities, {
2273
- include
2398
+ include,
2399
+ linkers: {
2400
+ document: new tsjapi.Linker(() => this.makeLinkUrl(`/${mappedType}`))
2401
+ }
2274
2402
  });
2275
2403
  const total = entities.length;
2276
2404
  body.meta = this.addTotalCountToMeta(body.meta, total);
@@ -2292,6 +2420,7 @@ var RestApiHandler = class {
2292
2420
  const options = {
2293
2421
  include,
2294
2422
  linkers: {
2423
+ document: new tsjapi.Linker(() => this.makeLinkUrl(`/${mappedType}`)),
2295
2424
  paginator: this.makePaginator(url, offset, limit, total)
2296
2425
  }
2297
2426
  };
@@ -2303,6 +2432,278 @@ var RestApiHandler = class {
2303
2432
  };
2304
2433
  }
2305
2434
  }
2435
+ /**
2436
+ * Builds link URL for a nested resource using parent type, parent ID, relation name, and optional child ID.
2437
+ * Uses the parent model name mapping for the parent segment; the relation name is used as-is.
2438
+ */
2439
+ makeNestedLinkUrl(parentType, parentId, parentRelation, childId) {
2440
+ const mappedParentType = this.mapModelName(parentType);
2441
+ const base = `/${mappedParentType}/${parentId}/${parentRelation}`;
2442
+ return childId ? `${base}/${childId}` : base;
2443
+ }
2444
+ async processNestedSingleRead(client, parentType, parentId, parentRelation, childId, query) {
2445
+ const resolved = this.resolveNestedRelation(parentType, parentRelation);
2446
+ if (!resolved) {
2447
+ return this.makeError("invalidPath");
2448
+ }
2449
+ const { filter: nestedFilter, error: nestedError } = this.buildNestedParentFilter(parentType, parentId, parentRelation);
2450
+ if (nestedError) return nestedError;
2451
+ const childType = resolved.childType;
2452
+ const typeInfo = this.getModelInfo(childType);
2453
+ const { args, include, error } = this.buildSingleReadArgs(childType, query);
2454
+ if (error) return error;
2455
+ args.where = this.mergeFilters(this.makeIdFilter(typeInfo.idFields, childId), nestedFilter);
2456
+ const entity = await client[childType].findFirst(args);
2457
+ if (!entity) return this.makeError("notFound");
2458
+ const linkUrl = this.makeLinkUrl(this.makeNestedLinkUrl(parentType, parentId, parentRelation, childId));
2459
+ const nestedLinker = new tsjapi.Linker(() => linkUrl);
2460
+ return {
2461
+ status: 200,
2462
+ body: await this.serializeItems(childType, entity, {
2463
+ include,
2464
+ linkers: {
2465
+ document: nestedLinker,
2466
+ resource: nestedLinker
2467
+ }
2468
+ })
2469
+ };
2470
+ }
2471
+ async processNestedCreate(client, parentType, parentId, parentRelation, _query, requestBody) {
2472
+ const resolved = this.resolveNestedRelation(parentType, parentRelation);
2473
+ if (!resolved) {
2474
+ return this.makeError("invalidPath");
2475
+ }
2476
+ const parentInfo = this.getModelInfo(parentType);
2477
+ const childType = resolved.childType;
2478
+ const childInfo = this.getModelInfo(childType);
2479
+ const { attributes, relationships, error } = this.processRequestBody(requestBody);
2480
+ if (error) return error;
2481
+ const createData = {
2482
+ ...attributes
2483
+ };
2484
+ if (relationships) {
2485
+ for (const [key, data] of Object.entries(relationships)) {
2486
+ if (!data?.data) {
2487
+ return this.makeError("invalidRelationData");
2488
+ }
2489
+ if (key === resolved.reverseRelation) {
2490
+ return this.makeError("invalidPayload", `Relation "${key}" is controlled by the parent route and cannot be set in the request payload`);
2491
+ }
2492
+ const relationInfo = childInfo.relationships[key];
2493
+ if (!relationInfo) {
2494
+ return this.makeUnsupportedRelationshipError(childType, key, 400);
2495
+ }
2496
+ if (relationInfo.isCollection) {
2497
+ createData[key] = {
2498
+ connect: enumerate(data.data).map((item) => this.makeIdConnect(relationInfo.idFields, item.id))
2499
+ };
2500
+ } else {
2501
+ if (typeof data.data !== "object") {
2502
+ return this.makeError("invalidRelationData");
2503
+ }
2504
+ createData[key] = {
2505
+ connect: this.makeIdConnect(relationInfo.idFields, data.data.id)
2506
+ };
2507
+ }
2508
+ }
2509
+ }
2510
+ const parentFkFields = Object.values(childInfo.fields).filter((f) => f.foreignKeyFor?.includes(resolved.reverseRelation));
2511
+ if (parentFkFields.some((f) => Object.prototype.hasOwnProperty.call(createData, f.name))) {
2512
+ return this.makeError("invalidPayload", `Relation "${resolved.reverseRelation}" is controlled by the parent route and cannot be set in the request payload`);
2513
+ }
2514
+ await client[parentType].update({
2515
+ where: this.makeIdFilter(parentInfo.idFields, parentId),
2516
+ data: {
2517
+ [parentRelation]: {
2518
+ create: createData
2519
+ }
2520
+ }
2521
+ });
2522
+ const { filter: nestedFilter, error: filterError } = this.buildNestedParentFilter(parentType, parentId, parentRelation);
2523
+ if (filterError) return filterError;
2524
+ const fetchArgs = {
2525
+ where: nestedFilter
2526
+ };
2527
+ this.includeRelationshipIds(childType, fetchArgs, "include");
2528
+ if (childInfo.idFields[0]) {
2529
+ fetchArgs.orderBy = {
2530
+ [childInfo.idFields[0].name]: "desc"
2531
+ };
2532
+ }
2533
+ const entity = await client[childType].findFirst(fetchArgs);
2534
+ if (!entity) return this.makeError("notFound");
2535
+ const collectionPath = this.makeNestedLinkUrl(parentType, parentId, parentRelation);
2536
+ const resourceLinker = new tsjapi.Linker((item) => this.makeLinkUrl(`${collectionPath}/${this.getId(childInfo.name, item)}`));
2537
+ return {
2538
+ status: 201,
2539
+ body: await this.serializeItems(childType, entity, {
2540
+ linkers: {
2541
+ document: resourceLinker,
2542
+ resource: resourceLinker
2543
+ }
2544
+ })
2545
+ };
2546
+ }
2547
+ /**
2548
+ * Builds the ORM `data` payload for a nested update, shared by both to-many (childId present)
2549
+ * and to-one (childId absent) variants. Returns either `{ updateData }` or `{ error }`.
2550
+ */
2551
+ buildNestedUpdatePayload(childType, typeInfo, rev, requestBody) {
2552
+ const { attributes, relationships, error } = this.processRequestBody(requestBody);
2553
+ if (error) return {
2554
+ error
2555
+ };
2556
+ const updateData = {
2557
+ ...attributes
2558
+ };
2559
+ if (relationships && Object.prototype.hasOwnProperty.call(relationships, rev)) {
2560
+ return {
2561
+ error: this.makeError("invalidPayload", `Relation "${rev}" cannot be changed via a nested route`)
2562
+ };
2563
+ }
2564
+ const fkFields = Object.values(typeInfo.fields).filter((f) => f.foreignKeyFor?.includes(rev));
2565
+ if (fkFields.some((f) => Object.prototype.hasOwnProperty.call(updateData, f.name))) {
2566
+ return {
2567
+ error: this.makeError("invalidPayload", `Relation "${rev}" cannot be changed via a nested route`)
2568
+ };
2569
+ }
2570
+ if (relationships) {
2571
+ for (const [key, data] of Object.entries(relationships)) {
2572
+ if (!data?.data) {
2573
+ return {
2574
+ error: this.makeError("invalidRelationData")
2575
+ };
2576
+ }
2577
+ const relationInfo = typeInfo.relationships[key];
2578
+ if (!relationInfo) {
2579
+ return {
2580
+ error: this.makeUnsupportedRelationshipError(childType, key, 400)
2581
+ };
2582
+ }
2583
+ if (relationInfo.isCollection) {
2584
+ updateData[key] = {
2585
+ set: enumerate(data.data).map((item) => ({
2586
+ [this.makeDefaultIdKey(relationInfo.idFields)]: item.id
2587
+ }))
2588
+ };
2589
+ } else {
2590
+ if (typeof data.data !== "object") {
2591
+ return {
2592
+ error: this.makeError("invalidRelationData")
2593
+ };
2594
+ }
2595
+ updateData[key] = {
2596
+ connect: {
2597
+ [this.makeDefaultIdKey(relationInfo.idFields)]: data.data.id
2598
+ }
2599
+ };
2600
+ }
2601
+ }
2602
+ }
2603
+ return {
2604
+ updateData
2605
+ };
2606
+ }
2607
+ /**
2608
+ * Handles PATCH /:type/:id/:relationship/:childId (to-many) and
2609
+ * PATCH /:type/:id/:relationship (to-one, childId undefined).
2610
+ */
2611
+ async processNestedUpdate(client, parentType, parentId, parentRelation, childId, _query, requestBody) {
2612
+ const resolved = this.resolveNestedRelation(parentType, parentRelation);
2613
+ if (!resolved) {
2614
+ return this.makeError("invalidPath");
2615
+ }
2616
+ const parentInfo = this.getModelInfo(parentType);
2617
+ const childType = resolved.childType;
2618
+ const typeInfo = this.getModelInfo(childType);
2619
+ const { updateData, error } = this.buildNestedUpdatePayload(childType, typeInfo, resolved.reverseRelation, requestBody);
2620
+ if (error) return error;
2621
+ if (childId) {
2622
+ await client[parentType].update({
2623
+ where: this.makeIdFilter(parentInfo.idFields, parentId),
2624
+ data: {
2625
+ [parentRelation]: {
2626
+ update: {
2627
+ where: this.makeIdFilter(typeInfo.idFields, childId),
2628
+ data: updateData
2629
+ }
2630
+ }
2631
+ }
2632
+ });
2633
+ const fetchArgs = {
2634
+ where: this.makeIdFilter(typeInfo.idFields, childId)
2635
+ };
2636
+ this.includeRelationshipIds(childType, fetchArgs, "include");
2637
+ const entity = await client[childType].findUnique(fetchArgs);
2638
+ if (!entity) return this.makeError("notFound");
2639
+ const linkUrl = this.makeLinkUrl(this.makeNestedLinkUrl(parentType, parentId, parentRelation, childId));
2640
+ const nestedLinker = new tsjapi.Linker(() => linkUrl);
2641
+ return {
2642
+ status: 200,
2643
+ body: await this.serializeItems(childType, entity, {
2644
+ linkers: {
2645
+ document: nestedLinker,
2646
+ resource: nestedLinker
2647
+ }
2648
+ })
2649
+ };
2650
+ } else {
2651
+ await client[parentType].update({
2652
+ where: this.makeIdFilter(parentInfo.idFields, parentId),
2653
+ data: {
2654
+ [parentRelation]: {
2655
+ update: updateData
2656
+ }
2657
+ }
2658
+ });
2659
+ const childIncludeArgs = {};
2660
+ this.includeRelationshipIds(childType, childIncludeArgs, "include");
2661
+ const fetchArgs = {
2662
+ where: this.makeIdFilter(parentInfo.idFields, parentId),
2663
+ select: {
2664
+ [parentRelation]: childIncludeArgs.include ? {
2665
+ include: childIncludeArgs.include
2666
+ } : true
2667
+ }
2668
+ };
2669
+ const parent = await client[parentType].findUnique(fetchArgs);
2670
+ const entity = parent?.[parentRelation];
2671
+ if (!entity) return this.makeError("notFound");
2672
+ const linkUrl = this.makeLinkUrl(this.makeNestedLinkUrl(parentType, parentId, parentRelation));
2673
+ const nestedLinker = new tsjapi.Linker(() => linkUrl);
2674
+ return {
2675
+ status: 200,
2676
+ body: await this.serializeItems(childType, entity, {
2677
+ linkers: {
2678
+ document: nestedLinker,
2679
+ resource: nestedLinker
2680
+ }
2681
+ })
2682
+ };
2683
+ }
2684
+ }
2685
+ async processNestedDelete(client, parentType, parentId, parentRelation, childId) {
2686
+ const resolved = this.resolveNestedRelation(parentType, parentRelation);
2687
+ if (!resolved) {
2688
+ return this.makeError("invalidPath");
2689
+ }
2690
+ const parentInfo = this.getModelInfo(parentType);
2691
+ const typeInfo = this.getModelInfo(resolved.childType);
2692
+ await client[parentType].update({
2693
+ where: this.makeIdFilter(parentInfo.idFields, parentId),
2694
+ data: {
2695
+ [parentRelation]: {
2696
+ delete: this.makeIdFilter(typeInfo.idFields, childId)
2697
+ }
2698
+ }
2699
+ });
2700
+ return {
2701
+ status: 200,
2702
+ body: {
2703
+ meta: {}
2704
+ }
2705
+ };
2706
+ }
2306
2707
  buildPartialSelect(type, query) {
2307
2708
  const selectFieldsQuery = query?.[`fields[${type}]`];
2308
2709
  if (!selectFieldsQuery) {