@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/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, "
|
|
2038
|
+
let match4 = this.matchUrlPattern(path, "relationship");
|
|
1947
2039
|
if (match4) {
|
|
1948
|
-
return await this.
|
|
2040
|
+
return await this.processRelationshipCRUD(client, "update", match4.type, match4.id, match4.relationship, query, requestBody);
|
|
1949
2041
|
}
|
|
1950
|
-
|
|
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.
|
|
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, "
|
|
2057
|
+
let match4 = this.matchUrlPattern(path, "relationship");
|
|
1958
2058
|
if (match4) {
|
|
1959
|
-
return await this.
|
|
2059
|
+
return await this.processRelationshipCRUD(client, "delete", match4.type, match4.id, match4.relationship, query, requestBody);
|
|
1960
2060
|
}
|
|
1961
|
-
|
|
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.
|
|
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
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
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
|
|
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
|
|
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) {
|