@rebasepro/server 0.10.1-canary.d8d45b2 → 0.10.1-canary.ed8caed
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/ast-schema-editor.d.ts +10 -0
- package/dist/api/rest/api-generator.d.ts +14 -0
- package/dist/index.es.js +226 -111
- package/dist/index.es.js.map +1 -1
- package/dist/{openapi-generator-DqSIwNLV.js → openapi-generator-C3_pTcsb.js} +2 -3
- package/dist/{openapi-generator-DqSIwNLV.js.map → openapi-generator-C3_pTcsb.js.map} +1 -1
- package/dist/{schema-editor-routes-D3ef8zu1.js → schema-editor-routes-CF-g0gVN.js} +30 -2
- package/dist/schema-editor-routes-CF-g0gVN.js.map +1 -0
- package/dist/{src-BITicbgD.js → src-DRUswG_w.js} +107 -3
- package/dist/src-DRUswG_w.js.map +1 -0
- package/dist/utils/dev-port.d.ts +5 -2
- package/package.json +8 -11
- package/dist/schema-editor-routes-D3ef8zu1.js.map +0 -1
- package/dist/src-BITicbgD.js.map +0 -1
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Move presentation keys into the `admin` block.
|
|
3
|
+
*
|
|
4
|
+
* `ADMIN_COLLECTION_KEYS` comes from `@rebasepro/types` rather than being spelled
|
|
5
|
+
* out here, so adding a field to `AdminCollectionOptions` cannot leave this writer
|
|
6
|
+
* behind. Any such key already inside `admin` wins over a top-level copy: the
|
|
7
|
+
* nested one is what the file said, and a flat duplicate is the view model's
|
|
8
|
+
* flattening leaking back.
|
|
9
|
+
*/
|
|
10
|
+
export declare function nestAdminKeys(collectionData: Record<string, unknown>): Record<string, unknown>;
|
|
1
11
|
export declare class AstSchemaEditor {
|
|
2
12
|
private project;
|
|
3
13
|
private collectionsDir;
|
|
@@ -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
|
@@ -2,7 +2,7 @@ import { createRequire as __createRequire } from "module";
|
|
|
2
2
|
import process from "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
4
|
import { _ as __toESM, a as generateRefreshToken, c as getRefreshTokenTtlMs, d as verifyAccessToken, f as verifyDownloadToken, g as __require, h as __exportAll, i as generateDownloadToken, l as hashRefreshToken, m as __commonJSMin, n as configureJwt, o as getAccessTokenExpiry, p as require_jsonwebtoken, r as generateAccessToken, s as getRefreshTokenExpiry, t as MAX_COOKIE_AGE_MS } from "./jwt-Dj7r7QX7.js";
|
|
5
|
-
import { _ as
|
|
5
|
+
import { S as RebaseClientError, _ as EntityReference, a as DEFAULT_DATA_SOURCE_KEY, b as Vector, c as policy, d as isPostgresCollectionConfig, g as toCanonicalOp, h as REST_TO_CANONICAL, i as DEFAULT_STORAGE_SOURCE_KEY, l as getCollectionDataPath, m as NULL_OPS, n as serializeCollections, o as getDataSourceCapabilities, p as CANONICAL_TO_REST, r as SCHEMA_VERSION_HEADER, s as isSQLAdmin, t as computeSchemaVersion, u as getDeclaredSubcollections, v as EntityRelation, x as RebaseApiError, y as GeoPoint } from "./src-DRUswG_w.js";
|
|
6
6
|
import { t as logger } from "./logger-BYU66ENZ.js";
|
|
7
7
|
import { t as nativeDynamicImport } from "./dynamic-import-Dvh-K5fl.js";
|
|
8
8
|
import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
|
|
@@ -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"]));
|
|
@@ -1689,39 +1783,9 @@ policy.or(policy.serverContext(), policy.rolesOverlap(["admin"]));
|
|
|
1689
1783
|
return jsonLogic;
|
|
1690
1784
|
});
|
|
1691
1785
|
})))();
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
* Default operators offered per property type, before engine capabilities and
|
|
1696
|
-
* per-property narrowing are applied. These mirror what the built-in filter
|
|
1697
|
-
* fields can render.
|
|
1698
|
-
*/
|
|
1699
|
-
var COMPARISON_OPS = [
|
|
1700
|
-
"==",
|
|
1701
|
-
"!=",
|
|
1702
|
-
">",
|
|
1703
|
-
">=",
|
|
1704
|
-
"<",
|
|
1705
|
-
"<="
|
|
1706
|
-
];
|
|
1707
|
-
var NULL_CHECK_OPS = ["is-null", "is-not-null"];
|
|
1708
|
-
var MEMBERSHIP_OPS = ["in", "not-in"];
|
|
1709
|
-
var PATTERN_OPS = [
|
|
1710
|
-
"like",
|
|
1711
|
-
"ilike",
|
|
1712
|
-
"not-like",
|
|
1713
|
-
"not-ilike"
|
|
1714
|
-
];
|
|
1715
|
-
[
|
|
1716
|
-
...COMPARISON_OPS,
|
|
1717
|
-
...MEMBERSHIP_OPS,
|
|
1718
|
-
...PATTERN_OPS,
|
|
1719
|
-
...NULL_CHECK_OPS
|
|
1720
|
-
], [
|
|
1721
|
-
...COMPARISON_OPS,
|
|
1722
|
-
...MEMBERSHIP_OPS,
|
|
1723
|
-
...NULL_CHECK_OPS
|
|
1724
|
-
], [...COMPARISON_OPS, ...NULL_CHECK_OPS], [...NULL_CHECK_OPS], [...MEMBERSHIP_OPS, ...NULL_CHECK_OPS], [...MEMBERSHIP_OPS, ...NULL_CHECK_OPS];
|
|
1786
|
+
/**
|
|
1787
|
+
* Apply PropertyConditions to a resolved property, evaluating all JSON Logic rules.
|
|
1788
|
+
*/
|
|
1725
1789
|
//#endregion
|
|
1726
1790
|
//#region ../../node_modules/.pnpm/fast-equals@6.0.0/node_modules/fast-equals/dist/es/index.mjs
|
|
1727
1791
|
var { getOwnPropertyNames, getOwnPropertySymbols } = Object;
|
|
@@ -2338,18 +2402,6 @@ var CollectionRegistry = class {
|
|
|
2338
2402
|
relResult.relations = mergedRelations;
|
|
2339
2403
|
}
|
|
2340
2404
|
result.properties = this.normalizeProperties(result.properties, mergedRelations);
|
|
2341
|
-
if (!result.childCollections) {
|
|
2342
|
-
const capabilities = getDataSourceCapabilities(result.engine);
|
|
2343
|
-
const declaredSubcollections = getDeclaredSubcollections(result);
|
|
2344
|
-
if (capabilities.supportsSubcollections && declaredSubcollections) result.childCollections = declaredSubcollections;
|
|
2345
|
-
else if (capabilities.supportsRelations && relResult.relations) {
|
|
2346
|
-
const manyRelations = relResult.relations.filter((r) => r.cardinality === "many");
|
|
2347
|
-
if (manyRelations.length > 0) result.childCollections = () => manyRelations.map((r) => {
|
|
2348
|
-
const target = r.target();
|
|
2349
|
-
return r.overrides ? mergeDeep(target, r.overrides) : target;
|
|
2350
|
-
});
|
|
2351
|
-
}
|
|
2352
|
-
}
|
|
2353
2405
|
return result;
|
|
2354
2406
|
}
|
|
2355
2407
|
/**
|
|
@@ -2448,9 +2500,7 @@ var CollectionRegistry = class {
|
|
|
2448
2500
|
const relation = findRelation(resolveCollectionRelations(currentCollection), relationKey);
|
|
2449
2501
|
if (!relation) throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
|
|
2450
2502
|
const target = relation.target();
|
|
2451
|
-
|
|
2452
|
-
const targetSlug = relation.overrides?.slug ?? targetRelationKey;
|
|
2453
|
-
currentCollection = this.get(targetSlug) || this.normalizeCollection(target);
|
|
2503
|
+
currentCollection = this.collectionsByTableName.get(getTableName(target)) ?? this.normalizeCollection(target);
|
|
2454
2504
|
if (i + 1 < pathSegments.length) {}
|
|
2455
2505
|
}
|
|
2456
2506
|
return currentCollection;
|
|
@@ -2485,7 +2535,7 @@ var CollectionRegistry = class {
|
|
|
2485
2535
|
if (!subcollections || subcollections.length === 0) throw new Error(`No subcollections found for ${currentCollection.slug} in path: ${path}`);
|
|
2486
2536
|
const subcollection = subcollections.find((c) => c.slug === subcollectionSlug);
|
|
2487
2537
|
if (!subcollection) throw new Error(`Subcollection '${subcollectionSlug}' not found in ${currentCollection.slug}`);
|
|
2488
|
-
currentCollection = this.
|
|
2538
|
+
currentCollection = this.normalizeCollection(subcollection);
|
|
2489
2539
|
collections.push(currentCollection);
|
|
2490
2540
|
}
|
|
2491
2541
|
}
|
|
@@ -4054,6 +4104,34 @@ var RestApiGenerator = class {
|
|
|
4054
4104
|
this.enforceApiKeyPermission(c, collectionPath.split("/").pop());
|
|
4055
4105
|
}
|
|
4056
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
|
+
/**
|
|
4057
4135
|
* Get the request-scoped driver. Throws if none is set — never falls
|
|
4058
4136
|
* back to the unscoped `this.driver` to avoid bypassing RLS/auth.
|
|
4059
4137
|
*/
|
|
@@ -4294,7 +4372,9 @@ var RestApiGenerator = class {
|
|
|
4294
4372
|
}) : 0;
|
|
4295
4373
|
return c.json({ count: total });
|
|
4296
4374
|
} else if (parsed.id) {
|
|
4297
|
-
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({
|
|
4298
4378
|
path: parsed.collectionPath,
|
|
4299
4379
|
id: parsed.id
|
|
4300
4380
|
});
|
|
@@ -4304,13 +4384,18 @@ var RestApiGenerator = class {
|
|
|
4304
4384
|
const queryDict = c.req.queries();
|
|
4305
4385
|
const queryOptions = this.parseQuery(queryDict);
|
|
4306
4386
|
const searchString = Array.isArray(queryDict.searchString) ? queryDict.searchString[queryDict.searchString.length - 1] : void 0;
|
|
4307
|
-
const
|
|
4308
|
-
|
|
4387
|
+
const fetchService = driver.restFetchService;
|
|
4388
|
+
const listOptions = {
|
|
4309
4389
|
filter: queryOptions.where,
|
|
4310
4390
|
limit: queryOptions.limit,
|
|
4391
|
+
offset: queryOptions.offset,
|
|
4311
4392
|
orderBy: queryOptions.orderBy?.[0]?.field,
|
|
4312
4393
|
order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
|
|
4313
4394
|
searchString
|
|
4395
|
+
};
|
|
4396
|
+
const entities = fetchService ? await fetchService.fetchCollectionForRest(parsed.collectionPath, listOptions, queryOptions.include) : await driver.fetchCollection({
|
|
4397
|
+
path: parsed.collectionPath,
|
|
4398
|
+
...listOptions
|
|
4314
4399
|
});
|
|
4315
4400
|
const total = driver.count ? await driver.count({
|
|
4316
4401
|
path: parsed.collectionPath,
|
|
@@ -4336,6 +4421,8 @@ var RestApiGenerator = class {
|
|
|
4336
4421
|
const driver = this.getScopedDriver(c);
|
|
4337
4422
|
this.enforceSubcollectionApiKeyPermission(c, parsed.collectionPath);
|
|
4338
4423
|
const body = await parseJsonBody(c);
|
|
4424
|
+
const targetCollection = this.resolveNestedWriteCollection(parsed.collectionPath);
|
|
4425
|
+
if (targetCollection) assertKnownWriteFields(body, targetCollection);
|
|
4339
4426
|
const entity = await driver.save({
|
|
4340
4427
|
path: parsed.collectionPath,
|
|
4341
4428
|
values: body,
|
|
@@ -4352,6 +4439,8 @@ var RestApiGenerator = class {
|
|
|
4352
4439
|
const driver = this.getScopedDriver(c);
|
|
4353
4440
|
this.enforceSubcollectionApiKeyPermission(c, parsed.collectionPath);
|
|
4354
4441
|
const body = await parseJsonBody(c);
|
|
4442
|
+
const targetCollection = this.resolveNestedWriteCollection(parsed.collectionPath);
|
|
4443
|
+
if (targetCollection) assertKnownWriteFields(body, targetCollection);
|
|
4355
4444
|
const entity = await driver.save({
|
|
4356
4445
|
path: parsed.collectionPath,
|
|
4357
4446
|
id: parsed.id,
|
|
@@ -12888,6 +12977,7 @@ async function verifyGoogleAccessToken(accessToken, expectedClientId) {
|
|
|
12888
12977
|
function createGoogleProvider(config) {
|
|
12889
12978
|
const clientId = typeof config === "string" ? config : config.clientId;
|
|
12890
12979
|
const clientSecret = typeof config === "string" ? void 0 : config.clientSecret;
|
|
12980
|
+
if (!clientSecret) console.warn("[Rebase] Google provider configured without a client secret. ID-token and access-token sign-in still work, but the authorization-code flow used by the admin login button will fail. Set GOOGLE_CLIENT_SECRET to enable it.");
|
|
12891
12981
|
let googleClient;
|
|
12892
12982
|
async function getClient() {
|
|
12893
12983
|
if (!googleClient) {
|
|
@@ -15044,7 +15134,7 @@ function assertStorageAccessControlConfigured(state, isProduction) {
|
|
|
15044
15134
|
//#region src/init/docs.ts
|
|
15045
15135
|
async function mountOpenApiDocs(app, basePath, enableSwagger, activeCollections, requireAuth) {
|
|
15046
15136
|
if (enableSwagger === false || activeCollections.length === 0) return;
|
|
15047
|
-
const { generateOpenApiSpec } = await import("./openapi-generator-
|
|
15137
|
+
const { generateOpenApiSpec } = await import("./openapi-generator-C3_pTcsb.js");
|
|
15048
15138
|
app.get(`${basePath}/docs`, (c) => {
|
|
15049
15139
|
const spec = generateOpenApiSpec(activeCollections, {
|
|
15050
15140
|
basePath,
|
|
@@ -15546,6 +15636,34 @@ function createAuth(transport, options) {
|
|
|
15546
15636
|
transport.setToken(null);
|
|
15547
15637
|
emit("SIGNED_OUT", null);
|
|
15548
15638
|
}
|
|
15639
|
+
/**
|
|
15640
|
+
* Recover from a 401 on an ordinary API request.
|
|
15641
|
+
*
|
|
15642
|
+
* Returns `true` when the caller should retry — we minted a fresh access
|
|
15643
|
+
* token. When the refresh is rejected *fatally* (the refresh token itself
|
|
15644
|
+
* is invalid, expired or revoked) this client can no longer act as the
|
|
15645
|
+
* user at all, so we drop the session and emit `SIGNED_OUT`. UIs gate on
|
|
15646
|
+
* that event, so they show their login screen instead of leaving the user
|
|
15647
|
+
* staring at "Invalid or expired token" on every view.
|
|
15648
|
+
*
|
|
15649
|
+
* Transient failures (offline, 5xx, backend restarting) keep the session:
|
|
15650
|
+
* the scheduled refresh backs off and retries, and the token is very
|
|
15651
|
+
* likely still good once the backend answers again.
|
|
15652
|
+
*/
|
|
15653
|
+
async function handleUnauthorized() {
|
|
15654
|
+
if (!currentSession) return false;
|
|
15655
|
+
if (authFlowMode !== "cookie" && !currentSession.refreshToken) {
|
|
15656
|
+
abandonSessionLocally();
|
|
15657
|
+
return false;
|
|
15658
|
+
}
|
|
15659
|
+
try {
|
|
15660
|
+
await refreshSession();
|
|
15661
|
+
return true;
|
|
15662
|
+
} catch (err) {
|
|
15663
|
+
if (isFatalRefreshError(err)) abandonSessionLocally();
|
|
15664
|
+
return false;
|
|
15665
|
+
}
|
|
15666
|
+
}
|
|
15549
15667
|
async function attemptScheduledRefresh(attempt) {
|
|
15550
15668
|
try {
|
|
15551
15669
|
await refreshSession();
|
|
@@ -16047,6 +16165,7 @@ function createAuth(transport, options) {
|
|
|
16047
16165
|
signInWithSpotify,
|
|
16048
16166
|
signOut,
|
|
16049
16167
|
refreshSession,
|
|
16168
|
+
handleUnauthorized,
|
|
16050
16169
|
getUser,
|
|
16051
16170
|
findUserByEmail,
|
|
16052
16171
|
updateUser,
|
|
@@ -20190,14 +20309,7 @@ function createRebaseClient(options) {
|
|
|
20190
20309
|
} catch (e) {}
|
|
20191
20310
|
return session?.accessToken || options.token || "";
|
|
20192
20311
|
},
|
|
20193
|
-
onUnauthorized: options.onUnauthorized || (
|
|
20194
|
-
try {
|
|
20195
|
-
await auth.refreshSession();
|
|
20196
|
-
return true;
|
|
20197
|
-
} catch (e) {
|
|
20198
|
-
return false;
|
|
20199
|
-
}
|
|
20200
|
-
})
|
|
20312
|
+
onUnauthorized: options.onUnauthorized || (() => auth.handleUnauthorized())
|
|
20201
20313
|
});
|
|
20202
20314
|
auth.onAuthStateChange((event, session) => {
|
|
20203
20315
|
if (!ws) return;
|
|
@@ -20207,14 +20319,7 @@ function createRebaseClient(options) {
|
|
|
20207
20319
|
}
|
|
20208
20320
|
});
|
|
20209
20321
|
}
|
|
20210
|
-
if (!options.onUnauthorized) transport.setOnUnauthorized(
|
|
20211
|
-
try {
|
|
20212
|
-
await auth.refreshSession();
|
|
20213
|
-
return true;
|
|
20214
|
-
} catch (e) {
|
|
20215
|
-
return false;
|
|
20216
|
-
}
|
|
20217
|
-
});
|
|
20322
|
+
if (!options.onUnauthorized) transport.setOnUnauthorized(() => auth.handleUnauthorized());
|
|
20218
20323
|
/**
|
|
20219
20324
|
* Suggest the closest known collection key for a mistyped accessor.
|
|
20220
20325
|
* Uses edit-distance-1 and prefix matching — no external dependency.
|
|
@@ -20952,7 +21057,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
20952
21057
|
if (schemaEditorEnabled && config.collectionsDir) {
|
|
20953
21058
|
let editorModule;
|
|
20954
21059
|
try {
|
|
20955
|
-
editorModule = await import("./schema-editor-routes-
|
|
21060
|
+
editorModule = await import("./schema-editor-routes-CF-g0gVN.js");
|
|
20956
21061
|
} catch (err) {
|
|
20957
21062
|
if (err?.code === "ERR_MODULE_NOT_FOUND") logger.warn("Schema Editor disabled: its dependency ts-morph is not installed. Run `npm install ts-morph@28.0.0` to enable it.");
|
|
20958
21063
|
else throw err;
|
|
@@ -22461,8 +22566,11 @@ var DEV_PORT_FILENAME = ".rebase-dev-port";
|
|
|
22461
22566
|
* Try to `listen` on `startPort`. If the port is busy (`EADDRINUSE`), increment
|
|
22462
22567
|
* and retry up to `maxAttempts` times.
|
|
22463
22568
|
*
|
|
22464
|
-
* When a port file
|
|
22465
|
-
*
|
|
22569
|
+
* When a port file written by a previous run exists *and that run asked for the
|
|
22570
|
+
* same `startPort`*, the port it landed on is tried first, so tsx watch restarts
|
|
22571
|
+
* keep the address the frontend was configured with. A different `startPort` means
|
|
22572
|
+
* the configuration changed and the file is ignored — an explicitly requested port
|
|
22573
|
+
* is never overridden by a stale one.
|
|
22466
22574
|
*
|
|
22467
22575
|
* Resolves with the port that was actually bound.
|
|
22468
22576
|
*
|
|
@@ -22490,8 +22598,10 @@ function listenWithPortRetry(server, startPort, options) {
|
|
|
22490
22598
|
if (portFileDir) try {
|
|
22491
22599
|
const portFile = path.join(portFileDir, DEV_PORT_FILENAME);
|
|
22492
22600
|
if (fs.existsSync(portFile)) {
|
|
22493
|
-
const
|
|
22494
|
-
|
|
22601
|
+
const [savedRaw, requestedRaw] = fs.readFileSync(portFile, "utf-8").trim().split(/\s+/);
|
|
22602
|
+
const saved = parseInt(savedRaw, 10);
|
|
22603
|
+
const requestedThen = requestedRaw === void 0 ? NaN : parseInt(requestedRaw, 10);
|
|
22604
|
+
if (saved > 0 && saved < 65536 && saved !== startPort && (Number.isNaN(requestedThen) || requestedThen === startPort)) affinityPort = saved;
|
|
22495
22605
|
}
|
|
22496
22606
|
} catch {}
|
|
22497
22607
|
return new Promise((resolve, reject) => {
|
|
@@ -22509,24 +22619,29 @@ function listenWithPortRetry(server, startPort, options) {
|
|
|
22509
22619
|
}
|
|
22510
22620
|
const port = portsToTry[index];
|
|
22511
22621
|
attempt++;
|
|
22512
|
-
const
|
|
22513
|
-
|
|
22514
|
-
server.removeListener("error", onError);
|
|
22515
|
-
tryNext(index + 1);
|
|
22516
|
-
} else reject(err);
|
|
22517
|
-
};
|
|
22518
|
-
server.once("error", onError);
|
|
22519
|
-
server.listen(port, host, () => {
|
|
22520
|
-
server.removeListener("error", onError);
|
|
22622
|
+
const onListening = () => {
|
|
22623
|
+
cleanup();
|
|
22521
22624
|
if (portFileDir) {
|
|
22522
22625
|
try {
|
|
22523
22626
|
const portFile = path.join(portFileDir, DEV_PORT_FILENAME);
|
|
22524
|
-
fs.writeFileSync(portFile,
|
|
22627
|
+
fs.writeFileSync(portFile, `${port} ${startPort}`, "utf-8");
|
|
22525
22628
|
} catch {}
|
|
22526
22629
|
writeStateFile(portFileDir, port, options?.serviceKey);
|
|
22527
22630
|
}
|
|
22528
22631
|
resolve(port);
|
|
22529
|
-
}
|
|
22632
|
+
};
|
|
22633
|
+
const onError = (err) => {
|
|
22634
|
+
cleanup();
|
|
22635
|
+
if (err.code === "EADDRINUSE") tryNext(index + 1);
|
|
22636
|
+
else reject(err);
|
|
22637
|
+
};
|
|
22638
|
+
function cleanup() {
|
|
22639
|
+
server.removeListener("listening", onListening);
|
|
22640
|
+
server.removeListener("error", onError);
|
|
22641
|
+
}
|
|
22642
|
+
server.once("error", onError);
|
|
22643
|
+
server.once("listening", onListening);
|
|
22644
|
+
server.listen(port, host);
|
|
22530
22645
|
}
|
|
22531
22646
|
tryNext(0);
|
|
22532
22647
|
});
|