@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 +40 -0
- package/dist/api.cjs +421 -20
- package/dist/api.cjs.map +1 -1
- package/dist/api.d.cts +46 -0
- package/dist/api.d.ts +46 -0
- package/dist/api.js +421 -20
- package/dist/api.js.map +1 -1
- package/package.json +7 -7
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, "
|
|
2073
|
+
let match4 = this.matchUrlPattern(path, "relationship");
|
|
1982
2074
|
if (match4) {
|
|
1983
|
-
return await this.
|
|
2075
|
+
return await this.processRelationshipCRUD(client, "update", match4.type, match4.id, match4.relationship, query, requestBody);
|
|
1984
2076
|
}
|
|
1985
|
-
|
|
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.
|
|
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, "
|
|
2092
|
+
let match4 = this.matchUrlPattern(path, "relationship");
|
|
1993
2093
|
if (match4) {
|
|
1994
|
-
return await this.
|
|
2094
|
+
return await this.processRelationshipCRUD(client, "delete", match4.type, match4.id, match4.relationship, query, requestBody);
|
|
1995
2095
|
}
|
|
1996
|
-
|
|
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.
|
|
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
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
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
|
|
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
|
|
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) {
|