@rebasepro/server 0.10.1-canary.8d27c46 → 0.10.1-canary.af2af91
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/rest/api-generator.d.ts +14 -0
- package/dist/index.es.js +163 -44
- package/dist/index.es.js.map +1 -1
- package/dist/src-DRUswG_w.js.map +1 -1
- package/package.json +5 -5
|
@@ -41,6 +41,20 @@ export declare class RestApiGenerator {
|
|
|
41
41
|
* slug, never an id.
|
|
42
42
|
*/
|
|
43
43
|
private enforceSubcollectionApiKeyPermission;
|
|
44
|
+
/**
|
|
45
|
+
* The collection a nested path writes into — the target of the relation its
|
|
46
|
+
* last segment names.
|
|
47
|
+
*
|
|
48
|
+
* Needed so a nested write can be checked against a schema at all. Without
|
|
49
|
+
* it these routes skipped `assertKnownWriteFields` entirely, which is why a
|
|
50
|
+
* typo `POST /posts` rejected with a 400 while the same typo on
|
|
51
|
+
* `POST /authors/1/posts` reached the database.
|
|
52
|
+
*
|
|
53
|
+
* Returns `undefined` rather than throwing when the path cannot be walked:
|
|
54
|
+
* the driver raises the authoritative error a moment later, and duplicating
|
|
55
|
+
* it here would report a resolution failure as a validation failure.
|
|
56
|
+
*/
|
|
57
|
+
private resolveNestedWriteCollection;
|
|
44
58
|
/**
|
|
45
59
|
* Get the request-scoped driver. Throws if none is set — never falls
|
|
46
60
|
* back to the unscoped `this.driver` to avoid bypassing RLS/auth.
|
package/dist/index.es.js
CHANGED
|
@@ -1309,6 +1309,19 @@ function sanitizeRelation(relation, sourceCollection, resolveCollection) {
|
|
|
1309
1309
|
if (newRelation.cardinality === "many" && newRelation.direction === "inverse" && !newRelation.foreignKeyOnTarget && !newRelation.joinPath && !newRelation.inverseRelationName) throw new Error(`Configuration Error in relation from '${sourceCollection.name}': An 'inverse' one-to-many relation requires a 'foreignKeyOnTarget'. Check the relation config for '${newRelation.relationName}'`);
|
|
1310
1310
|
return newRelation;
|
|
1311
1311
|
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Whether the target rows of this relation are reached through a junction — a
|
|
1314
|
+
* many-to-many, or a multi-hop `joinPath`.
|
|
1315
|
+
*
|
|
1316
|
+
* The distinction decides what a write "through" the relation may touch: a
|
|
1317
|
+
* junction-backed target is shared with other parents, so the parent owns the
|
|
1318
|
+
* *link* and not the row. The backend enforces that (an unlink rather than a
|
|
1319
|
+
* delete) and the admin renders it (remove-from-parent rather than delete), so
|
|
1320
|
+
* the predicate lives here rather than once per side.
|
|
1321
|
+
*/
|
|
1322
|
+
function isJunctionBackedRelation(relation) {
|
|
1323
|
+
return Boolean(relation.through) || Boolean(relation.joinPath && relation.joinPath.length > 1);
|
|
1324
|
+
}
|
|
1312
1325
|
/** WeakMap cache — same collection instance always yields the same relation map. */
|
|
1313
1326
|
var _resolvedRelationsCache = /* @__PURE__ */ new WeakMap();
|
|
1314
1327
|
function resolveCollectionRelations(collection) {
|
|
@@ -1385,34 +1398,115 @@ function findRelation(resolvedRelations, key) {
|
|
|
1385
1398
|
}
|
|
1386
1399
|
//#endregion
|
|
1387
1400
|
//#region ../common/src/util/resolutions.ts
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1401
|
+
/**
|
|
1402
|
+
* Relation identities that only ever appear *below* the top level of
|
|
1403
|
+
* `properties` — inside a `map`, an `array`, or a `oneOf` branch.
|
|
1404
|
+
*
|
|
1405
|
+
* A relation declared at the top level is a list hanging off the record and
|
|
1406
|
+
* earns a tab. One declared inside a map is a field of that map. Both end up in
|
|
1407
|
+
* the collection's flat `relations` array after normalization, so the shape of
|
|
1408
|
+
* `properties` is the only remaining evidence of which is which.
|
|
1409
|
+
*
|
|
1410
|
+
* An identity declared in both places is *not* nested-only, and keeps its tab.
|
|
1411
|
+
*/
|
|
1412
|
+
function getNestedRelationIdentities(properties) {
|
|
1413
|
+
const topLevel = /* @__PURE__ */ new Set();
|
|
1414
|
+
const nested = /* @__PURE__ */ new Set();
|
|
1415
|
+
const walk = (props, depth) => {
|
|
1416
|
+
if (!props) return;
|
|
1417
|
+
for (const [key, property] of Object.entries(props)) {
|
|
1418
|
+
if (!property) continue;
|
|
1419
|
+
if (property.type === "relation") {
|
|
1420
|
+
const relProp = property;
|
|
1421
|
+
(depth === 0 ? topLevel : nested).add(relProp.relationName ?? key);
|
|
1422
|
+
} else if (property.type === "map") walk(property.properties, depth + 1);
|
|
1423
|
+
else if (property.type === "array") {
|
|
1424
|
+
const arrayProp = property;
|
|
1425
|
+
if (Array.isArray(arrayProp.of)) arrayProp.of.forEach((p) => walk({ entry: p }, depth + 1));
|
|
1426
|
+
else if (arrayProp.of) walk({ entry: arrayProp.of }, depth + 1);
|
|
1427
|
+
if (arrayProp.oneOf?.properties) walk(arrayProp.oneOf.properties, depth + 1);
|
|
1402
1428
|
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
walk(properties, 0);
|
|
1432
|
+
for (const identity of topLevel) nested.delete(identity);
|
|
1433
|
+
return nested;
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* The lists rendered inside an entity view of `collection` — its tabs.
|
|
1437
|
+
*
|
|
1438
|
+
* The single derivation. There used to be two that disagreed: this one, and a
|
|
1439
|
+
* copy in `CollectionRegistry.normalizeCollection` that stamped each child with
|
|
1440
|
+
* the *target collection's* slug instead of the relation key. Since the
|
|
1441
|
+
* registry ran first and cached its answer onto `childCollections`, its version
|
|
1442
|
+
* was the one that won, and the frontend addressed child listings by a segment
|
|
1443
|
+
* the backend could not resolve.
|
|
1444
|
+
*
|
|
1445
|
+
* Order of precedence:
|
|
1446
|
+
* 1. `childCollections` — the explicit escape hatch for custom drivers.
|
|
1447
|
+
* 2. `subcollections` on an engine that has real containment (Firestore).
|
|
1448
|
+
* 3. many-relations on an engine that has relations (SQL).
|
|
1449
|
+
*/
|
|
1450
|
+
function getEntityChildViews(collection) {
|
|
1451
|
+
const asSubcollections = (collections) => collections.filter(Boolean).map((child) => ({
|
|
1452
|
+
key: child.slug,
|
|
1453
|
+
collection: child,
|
|
1454
|
+
source: { kind: "subcollection" }
|
|
1455
|
+
}));
|
|
1456
|
+
if (collection.childCollections) return asSubcollections(collection.childCollections() ?? []);
|
|
1457
|
+
const capabilities = getDataSourceCapabilities(collection.engine);
|
|
1458
|
+
const declaredSubcollections = getDeclaredSubcollections(collection);
|
|
1459
|
+
if (capabilities.supportsSubcollections && declaredSubcollections) return asSubcollections(declaredSubcollections() ?? []);
|
|
1460
|
+
if (!capabilities.supportsRelations) return [];
|
|
1461
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
1462
|
+
const nestedOnly = getNestedRelationIdentities(collection.properties);
|
|
1463
|
+
const views = [];
|
|
1464
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1465
|
+
for (const [relationKey, relation] of Object.entries(resolvedRelations)) {
|
|
1466
|
+
if (relation.cardinality !== "many") continue;
|
|
1467
|
+
const identity = relation.relationName ?? relationKey;
|
|
1468
|
+
if (seen.has(identity)) continue;
|
|
1469
|
+
if (nestedOnly.has(identity)) continue;
|
|
1470
|
+
let target;
|
|
1471
|
+
try {
|
|
1472
|
+
target = relation.target();
|
|
1473
|
+
} catch {
|
|
1474
|
+
continue;
|
|
1475
|
+
}
|
|
1476
|
+
if (!target) continue;
|
|
1477
|
+
seen.add(identity);
|
|
1478
|
+
const customName = Object.entries(collection.properties ?? {}).find(([propKey, p]) => p.type === "relation" && (p.relationName ?? propKey) === identity)?.[1]?.name;
|
|
1479
|
+
const base = {
|
|
1480
|
+
...target,
|
|
1481
|
+
slug: relationKey,
|
|
1482
|
+
...customName ? {
|
|
1483
|
+
name: customName,
|
|
1484
|
+
singularName: customName
|
|
1485
|
+
} : {}
|
|
1486
|
+
};
|
|
1487
|
+
views.push({
|
|
1488
|
+
key: relationKey,
|
|
1489
|
+
collection: relation.overrides ? mergeDeep(base, relation.overrides) : base,
|
|
1490
|
+
source: {
|
|
1491
|
+
kind: "relation",
|
|
1492
|
+
relationKey,
|
|
1493
|
+
mode: isJunctionBackedRelation(relation) ? "linked" : "owned",
|
|
1494
|
+
targetSlug: target.slug
|
|
1407
1495
|
}
|
|
1408
|
-
|
|
1409
|
-
...target,
|
|
1410
|
-
...baseOverrides
|
|
1411
|
-
};
|
|
1412
|
-
return r.overrides ? mergeDeep(targetWithOverrides, r.overrides) : targetWithOverrides;
|
|
1413
|
-
}).filter((c) => Boolean(c));
|
|
1496
|
+
});
|
|
1414
1497
|
}
|
|
1415
|
-
return
|
|
1498
|
+
return views;
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* The child views of `collection` as bare collections.
|
|
1502
|
+
*
|
|
1503
|
+
* The flattened view of {@link getEntityChildViews}, for navigation code that
|
|
1504
|
+
* only needs to match a path segment against a slug. Anything that cares *what
|
|
1505
|
+
* kind* of list it is showing — chiefly the admin, which must not offer a
|
|
1506
|
+
* global delete on a shared row — should read the views instead.
|
|
1507
|
+
*/
|
|
1508
|
+
function getSubcollections(collection) {
|
|
1509
|
+
return getEntityChildViews(collection).map((view) => view.collection);
|
|
1416
1510
|
}
|
|
1417
1511
|
policy.or(policy.serverContext(), policy.rolesOverlap(["admin"]));
|
|
1418
1512
|
policy.or(policy.serverContext(), policy.rolesOverlap(["admin"]));
|
|
@@ -2308,18 +2402,6 @@ var CollectionRegistry = class {
|
|
|
2308
2402
|
relResult.relations = mergedRelations;
|
|
2309
2403
|
}
|
|
2310
2404
|
result.properties = this.normalizeProperties(result.properties, mergedRelations);
|
|
2311
|
-
if (!result.childCollections) {
|
|
2312
|
-
const capabilities = getDataSourceCapabilities(result.engine);
|
|
2313
|
-
const declaredSubcollections = getDeclaredSubcollections(result);
|
|
2314
|
-
if (capabilities.supportsSubcollections && declaredSubcollections) result.childCollections = declaredSubcollections;
|
|
2315
|
-
else if (capabilities.supportsRelations && relResult.relations) {
|
|
2316
|
-
const manyRelations = relResult.relations.filter((r) => r.cardinality === "many");
|
|
2317
|
-
if (manyRelations.length > 0) result.childCollections = () => manyRelations.map((r) => {
|
|
2318
|
-
const target = r.target();
|
|
2319
|
-
return r.overrides ? mergeDeep(target, r.overrides) : target;
|
|
2320
|
-
});
|
|
2321
|
-
}
|
|
2322
|
-
}
|
|
2323
2405
|
return result;
|
|
2324
2406
|
}
|
|
2325
2407
|
/**
|
|
@@ -2418,9 +2500,7 @@ var CollectionRegistry = class {
|
|
|
2418
2500
|
const relation = findRelation(resolveCollectionRelations(currentCollection), relationKey);
|
|
2419
2501
|
if (!relation) throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
|
|
2420
2502
|
const target = relation.target();
|
|
2421
|
-
|
|
2422
|
-
const targetSlug = relation.overrides?.slug ?? targetRelationKey;
|
|
2423
|
-
currentCollection = this.get(targetSlug) || this.normalizeCollection(target);
|
|
2503
|
+
currentCollection = this.collectionsByTableName.get(getTableName(target)) ?? this.normalizeCollection(target);
|
|
2424
2504
|
if (i + 1 < pathSegments.length) {}
|
|
2425
2505
|
}
|
|
2426
2506
|
return currentCollection;
|
|
@@ -2455,7 +2535,7 @@ var CollectionRegistry = class {
|
|
|
2455
2535
|
if (!subcollections || subcollections.length === 0) throw new Error(`No subcollections found for ${currentCollection.slug} in path: ${path}`);
|
|
2456
2536
|
const subcollection = subcollections.find((c) => c.slug === subcollectionSlug);
|
|
2457
2537
|
if (!subcollection) throw new Error(`Subcollection '${subcollectionSlug}' not found in ${currentCollection.slug}`);
|
|
2458
|
-
currentCollection = this.
|
|
2538
|
+
currentCollection = this.normalizeCollection(subcollection);
|
|
2459
2539
|
collections.push(currentCollection);
|
|
2460
2540
|
}
|
|
2461
2541
|
}
|
|
@@ -4024,6 +4104,34 @@ var RestApiGenerator = class {
|
|
|
4024
4104
|
this.enforceApiKeyPermission(c, collectionPath.split("/").pop());
|
|
4025
4105
|
}
|
|
4026
4106
|
/**
|
|
4107
|
+
* The collection a nested path writes into — the target of the relation its
|
|
4108
|
+
* last segment names.
|
|
4109
|
+
*
|
|
4110
|
+
* Needed so a nested write can be checked against a schema at all. Without
|
|
4111
|
+
* it these routes skipped `assertKnownWriteFields` entirely, which is why a
|
|
4112
|
+
* typo `POST /posts` rejected with a 400 while the same typo on
|
|
4113
|
+
* `POST /authors/1/posts` reached the database.
|
|
4114
|
+
*
|
|
4115
|
+
* Returns `undefined` rather than throwing when the path cannot be walked:
|
|
4116
|
+
* the driver raises the authoritative error a moment later, and duplicating
|
|
4117
|
+
* it here would report a resolution failure as a validation failure.
|
|
4118
|
+
*/
|
|
4119
|
+
resolveNestedWriteCollection(collectionPath) {
|
|
4120
|
+
const segments = collectionPath.split("/").filter((s) => s && s !== "undefined");
|
|
4121
|
+
let current = this.collections.find((c) => c.slug === segments[0]);
|
|
4122
|
+
for (let i = 2; i < segments.length && current; i += 2) {
|
|
4123
|
+
const relation = findRelation(resolveCollectionRelations(current), segments[i]);
|
|
4124
|
+
if (!relation) return void 0;
|
|
4125
|
+
try {
|
|
4126
|
+
const target = relation.target();
|
|
4127
|
+
current = this.collections.find((c) => c.slug === target?.slug) ?? target;
|
|
4128
|
+
} catch {
|
|
4129
|
+
return;
|
|
4130
|
+
}
|
|
4131
|
+
}
|
|
4132
|
+
return current;
|
|
4133
|
+
}
|
|
4134
|
+
/**
|
|
4027
4135
|
* Get the request-scoped driver. Throws if none is set — never falls
|
|
4028
4136
|
* back to the unscoped `this.driver` to avoid bypassing RLS/auth.
|
|
4029
4137
|
*/
|
|
@@ -4264,7 +4372,9 @@ var RestApiGenerator = class {
|
|
|
4264
4372
|
}) : 0;
|
|
4265
4373
|
return c.json({ count: total });
|
|
4266
4374
|
} else if (parsed.id) {
|
|
4267
|
-
const
|
|
4375
|
+
const queryOptions = this.parseQuery(c.req.queries());
|
|
4376
|
+
const fetchService = driver.restFetchService;
|
|
4377
|
+
const entity = fetchService ? await fetchService.fetchOneForRest(parsed.collectionPath, parsed.id, queryOptions.include) : await driver.fetchOne({
|
|
4268
4378
|
path: parsed.collectionPath,
|
|
4269
4379
|
id: parsed.id
|
|
4270
4380
|
});
|
|
@@ -4274,13 +4384,18 @@ var RestApiGenerator = class {
|
|
|
4274
4384
|
const queryDict = c.req.queries();
|
|
4275
4385
|
const queryOptions = this.parseQuery(queryDict);
|
|
4276
4386
|
const searchString = Array.isArray(queryDict.searchString) ? queryDict.searchString[queryDict.searchString.length - 1] : void 0;
|
|
4277
|
-
const
|
|
4278
|
-
|
|
4387
|
+
const fetchService = driver.restFetchService;
|
|
4388
|
+
const listOptions = {
|
|
4279
4389
|
filter: queryOptions.where,
|
|
4280
4390
|
limit: queryOptions.limit,
|
|
4391
|
+
offset: queryOptions.offset,
|
|
4281
4392
|
orderBy: queryOptions.orderBy?.[0]?.field,
|
|
4282
4393
|
order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
|
|
4283
4394
|
searchString
|
|
4395
|
+
};
|
|
4396
|
+
const entities = fetchService ? await fetchService.fetchCollectionForRest(parsed.collectionPath, listOptions, queryOptions.include) : await driver.fetchCollection({
|
|
4397
|
+
path: parsed.collectionPath,
|
|
4398
|
+
...listOptions
|
|
4284
4399
|
});
|
|
4285
4400
|
const total = driver.count ? await driver.count({
|
|
4286
4401
|
path: parsed.collectionPath,
|
|
@@ -4306,6 +4421,8 @@ var RestApiGenerator = class {
|
|
|
4306
4421
|
const driver = this.getScopedDriver(c);
|
|
4307
4422
|
this.enforceSubcollectionApiKeyPermission(c, parsed.collectionPath);
|
|
4308
4423
|
const body = await parseJsonBody(c);
|
|
4424
|
+
const targetCollection = this.resolveNestedWriteCollection(parsed.collectionPath);
|
|
4425
|
+
if (targetCollection) assertKnownWriteFields(body, targetCollection);
|
|
4309
4426
|
const entity = await driver.save({
|
|
4310
4427
|
path: parsed.collectionPath,
|
|
4311
4428
|
values: body,
|
|
@@ -4322,6 +4439,8 @@ var RestApiGenerator = class {
|
|
|
4322
4439
|
const driver = this.getScopedDriver(c);
|
|
4323
4440
|
this.enforceSubcollectionApiKeyPermission(c, parsed.collectionPath);
|
|
4324
4441
|
const body = await parseJsonBody(c);
|
|
4442
|
+
const targetCollection = this.resolveNestedWriteCollection(parsed.collectionPath);
|
|
4443
|
+
if (targetCollection) assertKnownWriteFields(body, targetCollection);
|
|
4325
4444
|
const entity = await driver.save({
|
|
4326
4445
|
path: parsed.collectionPath,
|
|
4327
4446
|
id: parsed.id,
|