@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/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # @zenstackhq/server
2
+
3
+ Automatic CRUD API handlers and server adapters for ZenStack. Exposes your ZenStack ORM as RESTful or RPC-style API endpoints with built-in OpenAPI spec generation.
4
+
5
+ ## Supported Frameworks
6
+
7
+ - **Express**
8
+ - **Fastify**
9
+ - **Next.js**
10
+ - **Nuxt**
11
+ - **SvelteKit**
12
+ - **Hono**
13
+ - **Elysia**
14
+ - **TanStack Start**
15
+
16
+ ## API Styles
17
+
18
+ - **REST** — Resource-oriented endpoints with [JSON:API](https://jsonapi.org/) support
19
+ - **RPC** — Procedure-call style endpoints
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install @zenstackhq/server
25
+ ```
26
+
27
+ ## Usage (Express example)
28
+
29
+ ```typescript
30
+ import express from 'express';
31
+ import { ZenStackMiddleware } from '@zenstackhq/server/express';
32
+ import { RPCApiHandler } from '@zenstackhq/server/api';
33
+
34
+ const app = express();
35
+ app.use('/api/model', ZenStackMiddleware({...}));
36
+ ```
37
+
38
+ ## Learn More
39
+
40
+ - [ZenStack Documentation](https://zenstack.dev/docs)
package/dist/api.cjs CHANGED
@@ -1819,6 +1819,7 @@ var RestApiHandler = class {
1819
1819
  modelNameMapping;
1820
1820
  reverseModelNameMapping;
1821
1821
  externalIdMapping;
1822
+ nestedRoutes;
1822
1823
  constructor(options) {
1823
1824
  this.options = options;
1824
1825
  this.validateOptions(options);
@@ -1838,6 +1839,7 @@ var RestApiHandler = class {
1838
1839
  (0, import_common_helpers3.lowerCaseFirst)(k),
1839
1840
  v
1840
1841
  ]));
1842
+ this.nestedRoutes = options.nestedRoutes ?? false;
1841
1843
  this.urlPatternMap = this.buildUrlPatternMap(segmentCharset);
1842
1844
  this.buildTypeMap();
1843
1845
  this.buildSerializers();
@@ -1855,7 +1857,8 @@ var RestApiHandler = class {
1855
1857
  urlSegmentCharset: import_zod2.default.string().min(1).optional(),
1856
1858
  modelNameMapping: import_zod2.default.record(import_zod2.default.string(), import_zod2.default.string()).optional(),
1857
1859
  externalIdMapping: import_zod2.default.record(import_zod2.default.string(), import_zod2.default.string()).optional(),
1858
- queryOptions: queryOptionsSchema.optional()
1860
+ queryOptions: queryOptionsSchema.optional(),
1861
+ nestedRoutes: import_zod2.default.boolean().optional()
1859
1862
  });
1860
1863
  const parseResult = schema.safeParse(options);
1861
1864
  if (!parseResult.success) {
@@ -1880,6 +1883,12 @@ var RestApiHandler = class {
1880
1883
  ":type",
1881
1884
  ":id"
1882
1885
  ]), options),
1886
+ ["nestedSingle"]: new import_url_pattern.default(buildPath([
1887
+ ":type",
1888
+ ":id",
1889
+ ":relationship",
1890
+ ":childId"
1891
+ ]), options),
1883
1892
  ["fetchRelationship"]: new import_url_pattern.default(buildPath([
1884
1893
  ":type",
1885
1894
  ":id",
@@ -1899,6 +1908,81 @@ var RestApiHandler = class {
1899
1908
  mapModelName(modelName) {
1900
1909
  return this.modelNameMapping[modelName] ?? modelName;
1901
1910
  }
1911
+ /**
1912
+ * Resolves child model type and reverse relation from a parent relation name.
1913
+ * e.g. given parentType='user', parentRelation='posts', returns { childType:'post', reverseRelation:'author' }
1914
+ */
1915
+ resolveNestedRelation(parentType, parentRelation) {
1916
+ const parentInfo = this.getModelInfo(parentType);
1917
+ if (!parentInfo) return void 0;
1918
+ const field = this.schema.models[parentInfo.name]?.fields[parentRelation];
1919
+ if (!field?.relation) return void 0;
1920
+ const reverseRelation = field.relation.opposite;
1921
+ if (!reverseRelation) return void 0;
1922
+ return {
1923
+ childType: (0, import_common_helpers3.lowerCaseFirst)(field.type),
1924
+ reverseRelation,
1925
+ isCollection: !!field.array
1926
+ };
1927
+ }
1928
+ mergeFilters(left, right) {
1929
+ if (!left) {
1930
+ return right;
1931
+ }
1932
+ if (!right) {
1933
+ return left;
1934
+ }
1935
+ return {
1936
+ AND: [
1937
+ left,
1938
+ right
1939
+ ]
1940
+ };
1941
+ }
1942
+ /**
1943
+ * Builds a WHERE filter for the child model that constrains results to those belonging to the given parent.
1944
+ * @param parentType lowercased parent model name
1945
+ * @param parentId parent resource ID string
1946
+ * @param parentRelation relation field name on the parent model (e.g. 'posts')
1947
+ */
1948
+ buildNestedParentFilter(parentType, parentId, parentRelation) {
1949
+ const parentInfo = this.getModelInfo(parentType);
1950
+ if (!parentInfo) {
1951
+ return {
1952
+ filter: void 0,
1953
+ error: this.makeUnsupportedModelError(parentType)
1954
+ };
1955
+ }
1956
+ const resolved = this.resolveNestedRelation(parentType, parentRelation);
1957
+ if (!resolved) {
1958
+ return {
1959
+ filter: void 0,
1960
+ error: this.makeError("invalidPath", `invalid nested route: cannot resolve relation "${parentType}.${parentRelation}"`)
1961
+ };
1962
+ }
1963
+ const { reverseRelation } = resolved;
1964
+ const childInfo = this.getModelInfo(resolved.childType);
1965
+ if (!childInfo) {
1966
+ return {
1967
+ filter: void 0,
1968
+ error: this.makeUnsupportedModelError(resolved.childType)
1969
+ };
1970
+ }
1971
+ const reverseRelInfo = childInfo.relationships[reverseRelation];
1972
+ const relationFilter = reverseRelInfo?.isCollection ? {
1973
+ [reverseRelation]: {
1974
+ some: this.makeIdFilter(parentInfo.idFields, parentId, false)
1975
+ }
1976
+ } : {
1977
+ [reverseRelation]: {
1978
+ is: this.makeIdFilter(parentInfo.idFields, parentId, false)
1979
+ }
1980
+ };
1981
+ return {
1982
+ filter: relationFilter,
1983
+ error: void 0
1984
+ };
1985
+ }
1902
1986
  matchUrlPattern(path, routeType) {
1903
1987
  const pattern = this.urlPatternMap[routeType];
1904
1988
  if (!pattern) {
@@ -1946,6 +2030,10 @@ var RestApiHandler = class {
1946
2030
  if (match4) {
1947
2031
  return await this.processReadRelationship(client, match4.type, match4.id, match4.relationship, query);
1948
2032
  }
2033
+ match4 = this.matchUrlPattern(path, "nestedSingle");
2034
+ if (match4 && this.nestedRoutes && this.resolveNestedRelation(match4.type, match4.relationship)?.isCollection) {
2035
+ return await this.processNestedSingleRead(client, match4.type, match4.id, match4.relationship, match4.childId, query);
2036
+ }
1949
2037
  match4 = this.matchUrlPattern(path, "collection");
1950
2038
  if (match4) {
1951
2039
  return await this.processCollectionRead(client, match4.type, query);
@@ -1956,6 +2044,10 @@ var RestApiHandler = class {
1956
2044
  if (!requestBody) {
1957
2045
  return this.makeError("invalidPayload");
1958
2046
  }
2047
+ const nestedMatch = this.matchUrlPattern(path, "fetchRelationship");
2048
+ if (nestedMatch && this.nestedRoutes && this.resolveNestedRelation(nestedMatch.type, nestedMatch.relationship)?.isCollection) {
2049
+ return await this.processNestedCreate(client, nestedMatch.type, nestedMatch.id, nestedMatch.relationship, query, requestBody);
2050
+ }
1959
2051
  let match4 = this.matchUrlPattern(path, "collection");
1960
2052
  if (match4) {
1961
2053
  const body = requestBody;
@@ -1978,24 +2070,36 @@ var RestApiHandler = class {
1978
2070
  if (!requestBody) {
1979
2071
  return this.makeError("invalidPayload");
1980
2072
  }
1981
- let match4 = this.matchUrlPattern(path, "single");
2073
+ let match4 = this.matchUrlPattern(path, "relationship");
1982
2074
  if (match4) {
1983
- return await this.processUpdate(client, match4.type, match4.id, query, requestBody);
2075
+ return await this.processRelationshipCRUD(client, "update", match4.type, match4.id, match4.relationship, query, requestBody);
1984
2076
  }
1985
- match4 = this.matchUrlPattern(path, "relationship");
2077
+ const nestedToOnePatchMatch = this.matchUrlPattern(path, "fetchRelationship");
2078
+ if (nestedToOnePatchMatch && this.nestedRoutes && this.resolveNestedRelation(nestedToOnePatchMatch.type, nestedToOnePatchMatch.relationship) && !this.resolveNestedRelation(nestedToOnePatchMatch.type, nestedToOnePatchMatch.relationship)?.isCollection) {
2079
+ return await this.processNestedUpdate(client, nestedToOnePatchMatch.type, nestedToOnePatchMatch.id, nestedToOnePatchMatch.relationship, void 0, query, requestBody);
2080
+ }
2081
+ const nestedPatchMatch = this.matchUrlPattern(path, "nestedSingle");
2082
+ if (nestedPatchMatch && this.nestedRoutes && this.resolveNestedRelation(nestedPatchMatch.type, nestedPatchMatch.relationship)?.isCollection) {
2083
+ return await this.processNestedUpdate(client, nestedPatchMatch.type, nestedPatchMatch.id, nestedPatchMatch.relationship, nestedPatchMatch.childId, query, requestBody);
2084
+ }
2085
+ match4 = this.matchUrlPattern(path, "single");
1986
2086
  if (match4) {
1987
- return await this.processRelationshipCRUD(client, "update", match4.type, match4.id, match4.relationship, query, requestBody);
2087
+ return await this.processUpdate(client, match4.type, match4.id, query, requestBody);
1988
2088
  }
1989
2089
  return this.makeError("invalidPath");
1990
2090
  }
1991
2091
  case "DELETE": {
1992
- let match4 = this.matchUrlPattern(path, "single");
2092
+ let match4 = this.matchUrlPattern(path, "relationship");
1993
2093
  if (match4) {
1994
- return await this.processDelete(client, match4.type, match4.id);
2094
+ return await this.processRelationshipCRUD(client, "delete", match4.type, match4.id, match4.relationship, query, requestBody);
1995
2095
  }
1996
- match4 = this.matchUrlPattern(path, "relationship");
2096
+ const nestedDeleteMatch = this.matchUrlPattern(path, "nestedSingle");
2097
+ if (nestedDeleteMatch && this.nestedRoutes && this.resolveNestedRelation(nestedDeleteMatch.type, nestedDeleteMatch.relationship)?.isCollection) {
2098
+ return await this.processNestedDelete(client, nestedDeleteMatch.type, nestedDeleteMatch.id, nestedDeleteMatch.relationship, nestedDeleteMatch.childId);
2099
+ }
2100
+ match4 = this.matchUrlPattern(path, "single");
1997
2101
  if (match4) {
1998
- return await this.processRelationshipCRUD(client, "delete", match4.type, match4.id, match4.relationship, query, requestBody);
2102
+ return await this.processDelete(client, match4.type, match4.id);
1999
2103
  }
2000
2104
  return this.makeError("invalidPath");
2001
2105
  }
@@ -2082,20 +2186,23 @@ var RestApiHandler = class {
2082
2186
  log(this.log, "debug", () => `sending error response: ${(0, import_common_helpers3.safeJSONStringify)(resp)}${err instanceof Error ? "\n" + err.stack : ""}`);
2083
2187
  return resp;
2084
2188
  }
2085
- async processSingleRead(client, type, resourceId, query) {
2086
- const typeInfo = this.getModelInfo(type);
2087
- if (!typeInfo) {
2088
- return this.makeUnsupportedModelError(type);
2089
- }
2090
- const args = {
2091
- where: this.makeIdFilter(typeInfo.idFields, resourceId)
2092
- };
2189
+ /**
2190
+ * Builds the ORM `args` object (include, select) shared by single-read operations.
2191
+ * Returns the args to pass to findUnique/findFirst and the resolved `include` list for serialization,
2192
+ * or an error response if query params are invalid.
2193
+ */
2194
+ buildSingleReadArgs(type, query) {
2195
+ const args = {};
2093
2196
  this.includeRelationshipIds(type, args, "include");
2094
2197
  let include;
2095
2198
  if (query?.["include"]) {
2096
2199
  const { select: select2, error: error2, allIncludes } = this.buildRelationSelect(type, query["include"], query);
2097
2200
  if (error2) {
2098
- return error2;
2201
+ return {
2202
+ args,
2203
+ include,
2204
+ error: error2
2205
+ };
2099
2206
  }
2100
2207
  if (select2) {
2101
2208
  args.include = {
@@ -2106,7 +2213,11 @@ var RestApiHandler = class {
2106
2213
  include = allIncludes;
2107
2214
  }
2108
2215
  const { select, error } = this.buildPartialSelect(type, query);
2109
- if (error) return error;
2216
+ if (error) return {
2217
+ args,
2218
+ include,
2219
+ error
2220
+ };
2110
2221
  if (select) {
2111
2222
  args.select = {
2112
2223
  ...select,
@@ -2120,6 +2231,19 @@ var RestApiHandler = class {
2120
2231
  args.include = void 0;
2121
2232
  }
2122
2233
  }
2234
+ return {
2235
+ args,
2236
+ include
2237
+ };
2238
+ }
2239
+ async processSingleRead(client, type, resourceId, query) {
2240
+ const typeInfo = this.getModelInfo(type);
2241
+ if (!typeInfo) {
2242
+ return this.makeUnsupportedModelError(type);
2243
+ }
2244
+ const { args, include, error } = this.buildSingleReadArgs(type, query);
2245
+ if (error) return error;
2246
+ args.where = this.makeIdFilter(typeInfo.idFields, resourceId);
2123
2247
  const entity = await client[type].findUnique(args);
2124
2248
  if (entity) {
2125
2249
  return {
@@ -2304,8 +2428,12 @@ var RestApiHandler = class {
2304
2428
  }
2305
2429
  if (limit === Infinity) {
2306
2430
  const entities = await client[type].findMany(args);
2431
+ const mappedType = this.mapModelName(type);
2307
2432
  const body = await this.serializeItems(type, entities, {
2308
- include
2433
+ include,
2434
+ linkers: {
2435
+ document: new import_ts_japi.default.Linker(() => this.makeLinkUrl(`/${mappedType}`))
2436
+ }
2309
2437
  });
2310
2438
  const total = entities.length;
2311
2439
  body.meta = this.addTotalCountToMeta(body.meta, total);
@@ -2327,6 +2455,7 @@ var RestApiHandler = class {
2327
2455
  const options = {
2328
2456
  include,
2329
2457
  linkers: {
2458
+ document: new import_ts_japi.default.Linker(() => this.makeLinkUrl(`/${mappedType}`)),
2330
2459
  paginator: this.makePaginator(url, offset, limit, total)
2331
2460
  }
2332
2461
  };
@@ -2338,6 +2467,278 @@ var RestApiHandler = class {
2338
2467
  };
2339
2468
  }
2340
2469
  }
2470
+ /**
2471
+ * Builds link URL for a nested resource using parent type, parent ID, relation name, and optional child ID.
2472
+ * Uses the parent model name mapping for the parent segment; the relation name is used as-is.
2473
+ */
2474
+ makeNestedLinkUrl(parentType, parentId, parentRelation, childId) {
2475
+ const mappedParentType = this.mapModelName(parentType);
2476
+ const base = `/${mappedParentType}/${parentId}/${parentRelation}`;
2477
+ return childId ? `${base}/${childId}` : base;
2478
+ }
2479
+ async processNestedSingleRead(client, parentType, parentId, parentRelation, childId, query) {
2480
+ const resolved = this.resolveNestedRelation(parentType, parentRelation);
2481
+ if (!resolved) {
2482
+ return this.makeError("invalidPath");
2483
+ }
2484
+ const { filter: nestedFilter, error: nestedError } = this.buildNestedParentFilter(parentType, parentId, parentRelation);
2485
+ if (nestedError) return nestedError;
2486
+ const childType = resolved.childType;
2487
+ const typeInfo = this.getModelInfo(childType);
2488
+ const { args, include, error } = this.buildSingleReadArgs(childType, query);
2489
+ if (error) return error;
2490
+ args.where = this.mergeFilters(this.makeIdFilter(typeInfo.idFields, childId), nestedFilter);
2491
+ const entity = await client[childType].findFirst(args);
2492
+ if (!entity) return this.makeError("notFound");
2493
+ const linkUrl = this.makeLinkUrl(this.makeNestedLinkUrl(parentType, parentId, parentRelation, childId));
2494
+ const nestedLinker = new import_ts_japi.default.Linker(() => linkUrl);
2495
+ return {
2496
+ status: 200,
2497
+ body: await this.serializeItems(childType, entity, {
2498
+ include,
2499
+ linkers: {
2500
+ document: nestedLinker,
2501
+ resource: nestedLinker
2502
+ }
2503
+ })
2504
+ };
2505
+ }
2506
+ async processNestedCreate(client, parentType, parentId, parentRelation, _query, requestBody) {
2507
+ const resolved = this.resolveNestedRelation(parentType, parentRelation);
2508
+ if (!resolved) {
2509
+ return this.makeError("invalidPath");
2510
+ }
2511
+ const parentInfo = this.getModelInfo(parentType);
2512
+ const childType = resolved.childType;
2513
+ const childInfo = this.getModelInfo(childType);
2514
+ const { attributes, relationships, error } = this.processRequestBody(requestBody);
2515
+ if (error) return error;
2516
+ const createData = {
2517
+ ...attributes
2518
+ };
2519
+ if (relationships) {
2520
+ for (const [key, data] of Object.entries(relationships)) {
2521
+ if (!data?.data) {
2522
+ return this.makeError("invalidRelationData");
2523
+ }
2524
+ if (key === resolved.reverseRelation) {
2525
+ return this.makeError("invalidPayload", `Relation "${key}" is controlled by the parent route and cannot be set in the request payload`);
2526
+ }
2527
+ const relationInfo = childInfo.relationships[key];
2528
+ if (!relationInfo) {
2529
+ return this.makeUnsupportedRelationshipError(childType, key, 400);
2530
+ }
2531
+ if (relationInfo.isCollection) {
2532
+ createData[key] = {
2533
+ connect: (0, import_common_helpers3.enumerate)(data.data).map((item) => this.makeIdConnect(relationInfo.idFields, item.id))
2534
+ };
2535
+ } else {
2536
+ if (typeof data.data !== "object") {
2537
+ return this.makeError("invalidRelationData");
2538
+ }
2539
+ createData[key] = {
2540
+ connect: this.makeIdConnect(relationInfo.idFields, data.data.id)
2541
+ };
2542
+ }
2543
+ }
2544
+ }
2545
+ const parentFkFields = Object.values(childInfo.fields).filter((f) => f.foreignKeyFor?.includes(resolved.reverseRelation));
2546
+ if (parentFkFields.some((f) => Object.prototype.hasOwnProperty.call(createData, f.name))) {
2547
+ return this.makeError("invalidPayload", `Relation "${resolved.reverseRelation}" is controlled by the parent route and cannot be set in the request payload`);
2548
+ }
2549
+ await client[parentType].update({
2550
+ where: this.makeIdFilter(parentInfo.idFields, parentId),
2551
+ data: {
2552
+ [parentRelation]: {
2553
+ create: createData
2554
+ }
2555
+ }
2556
+ });
2557
+ const { filter: nestedFilter, error: filterError } = this.buildNestedParentFilter(parentType, parentId, parentRelation);
2558
+ if (filterError) return filterError;
2559
+ const fetchArgs = {
2560
+ where: nestedFilter
2561
+ };
2562
+ this.includeRelationshipIds(childType, fetchArgs, "include");
2563
+ if (childInfo.idFields[0]) {
2564
+ fetchArgs.orderBy = {
2565
+ [childInfo.idFields[0].name]: "desc"
2566
+ };
2567
+ }
2568
+ const entity = await client[childType].findFirst(fetchArgs);
2569
+ if (!entity) return this.makeError("notFound");
2570
+ const collectionPath = this.makeNestedLinkUrl(parentType, parentId, parentRelation);
2571
+ const resourceLinker = new import_ts_japi.default.Linker((item) => this.makeLinkUrl(`${collectionPath}/${this.getId(childInfo.name, item)}`));
2572
+ return {
2573
+ status: 201,
2574
+ body: await this.serializeItems(childType, entity, {
2575
+ linkers: {
2576
+ document: resourceLinker,
2577
+ resource: resourceLinker
2578
+ }
2579
+ })
2580
+ };
2581
+ }
2582
+ /**
2583
+ * Builds the ORM `data` payload for a nested update, shared by both to-many (childId present)
2584
+ * and to-one (childId absent) variants. Returns either `{ updateData }` or `{ error }`.
2585
+ */
2586
+ buildNestedUpdatePayload(childType, typeInfo, rev, requestBody) {
2587
+ const { attributes, relationships, error } = this.processRequestBody(requestBody);
2588
+ if (error) return {
2589
+ error
2590
+ };
2591
+ const updateData = {
2592
+ ...attributes
2593
+ };
2594
+ if (relationships && Object.prototype.hasOwnProperty.call(relationships, rev)) {
2595
+ return {
2596
+ error: this.makeError("invalidPayload", `Relation "${rev}" cannot be changed via a nested route`)
2597
+ };
2598
+ }
2599
+ const fkFields = Object.values(typeInfo.fields).filter((f) => f.foreignKeyFor?.includes(rev));
2600
+ if (fkFields.some((f) => Object.prototype.hasOwnProperty.call(updateData, f.name))) {
2601
+ return {
2602
+ error: this.makeError("invalidPayload", `Relation "${rev}" cannot be changed via a nested route`)
2603
+ };
2604
+ }
2605
+ if (relationships) {
2606
+ for (const [key, data] of Object.entries(relationships)) {
2607
+ if (!data?.data) {
2608
+ return {
2609
+ error: this.makeError("invalidRelationData")
2610
+ };
2611
+ }
2612
+ const relationInfo = typeInfo.relationships[key];
2613
+ if (!relationInfo) {
2614
+ return {
2615
+ error: this.makeUnsupportedRelationshipError(childType, key, 400)
2616
+ };
2617
+ }
2618
+ if (relationInfo.isCollection) {
2619
+ updateData[key] = {
2620
+ set: (0, import_common_helpers3.enumerate)(data.data).map((item) => ({
2621
+ [this.makeDefaultIdKey(relationInfo.idFields)]: item.id
2622
+ }))
2623
+ };
2624
+ } else {
2625
+ if (typeof data.data !== "object") {
2626
+ return {
2627
+ error: this.makeError("invalidRelationData")
2628
+ };
2629
+ }
2630
+ updateData[key] = {
2631
+ connect: {
2632
+ [this.makeDefaultIdKey(relationInfo.idFields)]: data.data.id
2633
+ }
2634
+ };
2635
+ }
2636
+ }
2637
+ }
2638
+ return {
2639
+ updateData
2640
+ };
2641
+ }
2642
+ /**
2643
+ * Handles PATCH /:type/:id/:relationship/:childId (to-many) and
2644
+ * PATCH /:type/:id/:relationship (to-one, childId undefined).
2645
+ */
2646
+ async processNestedUpdate(client, parentType, parentId, parentRelation, childId, _query, requestBody) {
2647
+ const resolved = this.resolveNestedRelation(parentType, parentRelation);
2648
+ if (!resolved) {
2649
+ return this.makeError("invalidPath");
2650
+ }
2651
+ const parentInfo = this.getModelInfo(parentType);
2652
+ const childType = resolved.childType;
2653
+ const typeInfo = this.getModelInfo(childType);
2654
+ const { updateData, error } = this.buildNestedUpdatePayload(childType, typeInfo, resolved.reverseRelation, requestBody);
2655
+ if (error) return error;
2656
+ if (childId) {
2657
+ await client[parentType].update({
2658
+ where: this.makeIdFilter(parentInfo.idFields, parentId),
2659
+ data: {
2660
+ [parentRelation]: {
2661
+ update: {
2662
+ where: this.makeIdFilter(typeInfo.idFields, childId),
2663
+ data: updateData
2664
+ }
2665
+ }
2666
+ }
2667
+ });
2668
+ const fetchArgs = {
2669
+ where: this.makeIdFilter(typeInfo.idFields, childId)
2670
+ };
2671
+ this.includeRelationshipIds(childType, fetchArgs, "include");
2672
+ const entity = await client[childType].findUnique(fetchArgs);
2673
+ if (!entity) return this.makeError("notFound");
2674
+ const linkUrl = this.makeLinkUrl(this.makeNestedLinkUrl(parentType, parentId, parentRelation, childId));
2675
+ const nestedLinker = new import_ts_japi.default.Linker(() => linkUrl);
2676
+ return {
2677
+ status: 200,
2678
+ body: await this.serializeItems(childType, entity, {
2679
+ linkers: {
2680
+ document: nestedLinker,
2681
+ resource: nestedLinker
2682
+ }
2683
+ })
2684
+ };
2685
+ } else {
2686
+ await client[parentType].update({
2687
+ where: this.makeIdFilter(parentInfo.idFields, parentId),
2688
+ data: {
2689
+ [parentRelation]: {
2690
+ update: updateData
2691
+ }
2692
+ }
2693
+ });
2694
+ const childIncludeArgs = {};
2695
+ this.includeRelationshipIds(childType, childIncludeArgs, "include");
2696
+ const fetchArgs = {
2697
+ where: this.makeIdFilter(parentInfo.idFields, parentId),
2698
+ select: {
2699
+ [parentRelation]: childIncludeArgs.include ? {
2700
+ include: childIncludeArgs.include
2701
+ } : true
2702
+ }
2703
+ };
2704
+ const parent = await client[parentType].findUnique(fetchArgs);
2705
+ const entity = parent?.[parentRelation];
2706
+ if (!entity) return this.makeError("notFound");
2707
+ const linkUrl = this.makeLinkUrl(this.makeNestedLinkUrl(parentType, parentId, parentRelation));
2708
+ const nestedLinker = new import_ts_japi.default.Linker(() => linkUrl);
2709
+ return {
2710
+ status: 200,
2711
+ body: await this.serializeItems(childType, entity, {
2712
+ linkers: {
2713
+ document: nestedLinker,
2714
+ resource: nestedLinker
2715
+ }
2716
+ })
2717
+ };
2718
+ }
2719
+ }
2720
+ async processNestedDelete(client, parentType, parentId, parentRelation, childId) {
2721
+ const resolved = this.resolveNestedRelation(parentType, parentRelation);
2722
+ if (!resolved) {
2723
+ return this.makeError("invalidPath");
2724
+ }
2725
+ const parentInfo = this.getModelInfo(parentType);
2726
+ const typeInfo = this.getModelInfo(resolved.childType);
2727
+ await client[parentType].update({
2728
+ where: this.makeIdFilter(parentInfo.idFields, parentId),
2729
+ data: {
2730
+ [parentRelation]: {
2731
+ delete: this.makeIdFilter(typeInfo.idFields, childId)
2732
+ }
2733
+ }
2734
+ });
2735
+ return {
2736
+ status: 200,
2737
+ body: {
2738
+ meta: {}
2739
+ }
2740
+ };
2741
+ }
2341
2742
  buildPartialSelect(type, query) {
2342
2743
  const selectFieldsQuery = query?.[`fields[${type}]`];
2343
2744
  if (!selectFieldsQuery) {