@rebasepro/server 0.10.1-canary.b1e3dbf → 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 +260 -114
- 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/storage/routes.d.ts +9 -1
- package/dist/storage/types.d.ts +1 -1
- 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) {
|
|
@@ -14580,7 +14670,7 @@ function buildAdapterAuthMiddleware(adapter, requireAuth, publicRead) {
|
|
|
14580
14670
|
function createStorageRoutes(config) {
|
|
14581
14671
|
const router = new Hono();
|
|
14582
14672
|
router.onError(errorHandler);
|
|
14583
|
-
const { controller, registry, sources: declaredSources, requireAuth: requireAuth$1 = true, publicRead = false, authAdapter, authorize } = config;
|
|
14673
|
+
const { controller, registry, sources: declaredSources, requireAuth: requireAuth$1 = true, publicRead = false, authAdapter, authorize, authorizeData } = config;
|
|
14584
14674
|
/**
|
|
14585
14675
|
* Run the per-object authorization hook, if one is configured.
|
|
14586
14676
|
*
|
|
@@ -14601,7 +14691,8 @@ function createStorageRoutes(config) {
|
|
|
14601
14691
|
bucket,
|
|
14602
14692
|
operation,
|
|
14603
14693
|
user,
|
|
14604
|
-
storageId: storageId ?? void 0
|
|
14694
|
+
storageId: storageId ?? void 0,
|
|
14695
|
+
data: authorizeData?.()
|
|
14605
14696
|
});
|
|
14606
14697
|
} catch {
|
|
14607
14698
|
allowed = false;
|
|
@@ -15043,7 +15134,7 @@ function assertStorageAccessControlConfigured(state, isProduction) {
|
|
|
15043
15134
|
//#region src/init/docs.ts
|
|
15044
15135
|
async function mountOpenApiDocs(app, basePath, enableSwagger, activeCollections, requireAuth) {
|
|
15045
15136
|
if (enableSwagger === false || activeCollections.length === 0) return;
|
|
15046
|
-
const { generateOpenApiSpec } = await import("./openapi-generator-
|
|
15137
|
+
const { generateOpenApiSpec } = await import("./openapi-generator-C3_pTcsb.js");
|
|
15047
15138
|
app.get(`${basePath}/docs`, (c) => {
|
|
15048
15139
|
const spec = generateOpenApiSpec(activeCollections, {
|
|
15049
15140
|
basePath,
|
|
@@ -15545,6 +15636,34 @@ function createAuth(transport, options) {
|
|
|
15545
15636
|
transport.setToken(null);
|
|
15546
15637
|
emit("SIGNED_OUT", null);
|
|
15547
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
|
+
}
|
|
15548
15667
|
async function attemptScheduledRefresh(attempt) {
|
|
15549
15668
|
try {
|
|
15550
15669
|
await refreshSession();
|
|
@@ -16046,6 +16165,7 @@ function createAuth(transport, options) {
|
|
|
16046
16165
|
signInWithSpotify,
|
|
16047
16166
|
signOut,
|
|
16048
16167
|
refreshSession,
|
|
16168
|
+
handleUnauthorized,
|
|
16049
16169
|
getUser,
|
|
16050
16170
|
findUserByEmail,
|
|
16051
16171
|
updateUser,
|
|
@@ -20189,14 +20309,7 @@ function createRebaseClient(options) {
|
|
|
20189
20309
|
} catch (e) {}
|
|
20190
20310
|
return session?.accessToken || options.token || "";
|
|
20191
20311
|
},
|
|
20192
|
-
onUnauthorized: options.onUnauthorized || (
|
|
20193
|
-
try {
|
|
20194
|
-
await auth.refreshSession();
|
|
20195
|
-
return true;
|
|
20196
|
-
} catch (e) {
|
|
20197
|
-
return false;
|
|
20198
|
-
}
|
|
20199
|
-
})
|
|
20312
|
+
onUnauthorized: options.onUnauthorized || (() => auth.handleUnauthorized())
|
|
20200
20313
|
});
|
|
20201
20314
|
auth.onAuthStateChange((event, session) => {
|
|
20202
20315
|
if (!ws) return;
|
|
@@ -20206,14 +20319,7 @@ function createRebaseClient(options) {
|
|
|
20206
20319
|
}
|
|
20207
20320
|
});
|
|
20208
20321
|
}
|
|
20209
|
-
if (!options.onUnauthorized) transport.setOnUnauthorized(
|
|
20210
|
-
try {
|
|
20211
|
-
await auth.refreshSession();
|
|
20212
|
-
return true;
|
|
20213
|
-
} catch (e) {
|
|
20214
|
-
return false;
|
|
20215
|
-
}
|
|
20216
|
-
});
|
|
20322
|
+
if (!options.onUnauthorized) transport.setOnUnauthorized(() => auth.handleUnauthorized());
|
|
20217
20323
|
/**
|
|
20218
20324
|
* Suggest the closest known collection key for a mistyped accessor.
|
|
20219
20325
|
* Uses edit-distance-1 and prefix matching — no external dependency.
|
|
@@ -20951,7 +21057,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
20951
21057
|
if (schemaEditorEnabled && config.collectionsDir) {
|
|
20952
21058
|
let editorModule;
|
|
20953
21059
|
try {
|
|
20954
|
-
editorModule = await import("./schema-editor-routes-
|
|
21060
|
+
editorModule = await import("./schema-editor-routes-CF-g0gVN.js");
|
|
20955
21061
|
} catch (err) {
|
|
20956
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.");
|
|
20957
21063
|
else throw err;
|
|
@@ -20963,6 +21069,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
20963
21069
|
logger.info("Schema Editor mounted", { path: `${basePath}/schema-editor` });
|
|
20964
21070
|
}
|
|
20965
21071
|
}
|
|
21072
|
+
const storageAuthorizeData = {};
|
|
20966
21073
|
if (storageController) {
|
|
20967
21074
|
const storageMaxSize = (config.storage && typeof config.storage === "object" && "type" in config.storage ? config.storage.maxFileSize : void 0) ?? 50 * 1024 * 1024;
|
|
20968
21075
|
assertStorageAccessControlConfigured({
|
|
@@ -20977,7 +21084,8 @@ async function _initializeRebaseBackend(config) {
|
|
|
20977
21084
|
requireAuth: resolveRequireAuth(config.auth),
|
|
20978
21085
|
publicRead: config.storagePublicRead === true,
|
|
20979
21086
|
authAdapter,
|
|
20980
|
-
authorize: config.storageAuthorize
|
|
21087
|
+
authorize: config.storageAuthorize,
|
|
21088
|
+
authorizeData: () => storageAuthorizeData.current
|
|
20981
21089
|
});
|
|
20982
21090
|
const storageRouter = new Hono();
|
|
20983
21091
|
if (apiKeyPreAuth) storageRouter.use("/*", apiKeyPreAuth, createStorageApiKeyGuard());
|
|
@@ -21056,6 +21164,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
21056
21164
|
roles: ["admin"]
|
|
21057
21165
|
};
|
|
21058
21166
|
const defaultData = buildSdkData(await scopeDataDriver(defaultDriver, serviceIdentity));
|
|
21167
|
+
storageAuthorizeData.current = defaultData;
|
|
21059
21168
|
const dataSourcesByKey = {};
|
|
21060
21169
|
for (const driverKey of driverRegistry.list()) {
|
|
21061
21170
|
if (driverKey === "(default)") continue;
|
|
@@ -22457,8 +22566,11 @@ var DEV_PORT_FILENAME = ".rebase-dev-port";
|
|
|
22457
22566
|
* Try to `listen` on `startPort`. If the port is busy (`EADDRINUSE`), increment
|
|
22458
22567
|
* and retry up to `maxAttempts` times.
|
|
22459
22568
|
*
|
|
22460
|
-
* When a port file
|
|
22461
|
-
*
|
|
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.
|
|
22462
22574
|
*
|
|
22463
22575
|
* Resolves with the port that was actually bound.
|
|
22464
22576
|
*
|
|
@@ -22486,8 +22598,10 @@ function listenWithPortRetry(server, startPort, options) {
|
|
|
22486
22598
|
if (portFileDir) try {
|
|
22487
22599
|
const portFile = path.join(portFileDir, DEV_PORT_FILENAME);
|
|
22488
22600
|
if (fs.existsSync(portFile)) {
|
|
22489
|
-
const
|
|
22490
|
-
|
|
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;
|
|
22491
22605
|
}
|
|
22492
22606
|
} catch {}
|
|
22493
22607
|
return new Promise((resolve, reject) => {
|
|
@@ -22505,24 +22619,29 @@ function listenWithPortRetry(server, startPort, options) {
|
|
|
22505
22619
|
}
|
|
22506
22620
|
const port = portsToTry[index];
|
|
22507
22621
|
attempt++;
|
|
22508
|
-
const
|
|
22509
|
-
|
|
22510
|
-
server.removeListener("error", onError);
|
|
22511
|
-
tryNext(index + 1);
|
|
22512
|
-
} else reject(err);
|
|
22513
|
-
};
|
|
22514
|
-
server.once("error", onError);
|
|
22515
|
-
server.listen(port, host, () => {
|
|
22516
|
-
server.removeListener("error", onError);
|
|
22622
|
+
const onListening = () => {
|
|
22623
|
+
cleanup();
|
|
22517
22624
|
if (portFileDir) {
|
|
22518
22625
|
try {
|
|
22519
22626
|
const portFile = path.join(portFileDir, DEV_PORT_FILENAME);
|
|
22520
|
-
fs.writeFileSync(portFile,
|
|
22627
|
+
fs.writeFileSync(portFile, `${port} ${startPort}`, "utf-8");
|
|
22521
22628
|
} catch {}
|
|
22522
22629
|
writeStateFile(portFileDir, port, options?.serviceKey);
|
|
22523
22630
|
}
|
|
22524
22631
|
resolve(port);
|
|
22525
|
-
}
|
|
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);
|
|
22526
22645
|
}
|
|
22527
22646
|
tryNext(0);
|
|
22528
22647
|
});
|
|
@@ -23744,6 +23863,7 @@ async function bootFromBundle(options = {}) {
|
|
|
23744
23863
|
const server = createServer(getRequestListener(app.fetch));
|
|
23745
23864
|
const usersCollection = await loadUsersCollection(bundle);
|
|
23746
23865
|
const storage = resolveStorageSources(process.env, storageSourceDefs, path.join(bundle.dir, "uploads"));
|
|
23866
|
+
await ensureCollectionSchema(bundle, dataSources, env);
|
|
23747
23867
|
const backend = await initializeRebaseBackend({
|
|
23748
23868
|
server,
|
|
23749
23869
|
app,
|
|
@@ -23953,6 +24073,32 @@ async function runFromBundle(options = {}) {
|
|
|
23953
24073
|
process.exit(1);
|
|
23954
24074
|
}
|
|
23955
24075
|
}
|
|
24076
|
+
/**
|
|
24077
|
+
* Bring the database's collection tables up to date before serving.
|
|
24078
|
+
*
|
|
24079
|
+
* Delegates to whichever driver bootstrapped the default data source; a driver
|
|
24080
|
+
* without `ensureCollectionSchema` (a schemaless one, or an older build) simply
|
|
24081
|
+
* skips, which is why this cannot break an existing deployment.
|
|
24082
|
+
*
|
|
24083
|
+
* Failure is fatal on purpose. Booting anyway would produce exactly the state
|
|
24084
|
+
* this exists to prevent — an app that answers sign-in and 500s every data
|
|
24085
|
+
* request — and a crash-looping pod with the DDL error in its logs is a far
|
|
24086
|
+
* better signal than a running one that silently cannot serve.
|
|
24087
|
+
*/
|
|
24088
|
+
async function ensureCollectionSchema(bundle, dataSources, env) {
|
|
24089
|
+
if ((env.REBASE_MIGRATE_ON_BOOT || "ensure") === "none") {
|
|
24090
|
+
logger.info("REBASE_MIGRATE_ON_BOOT=none — leaving the database schema untouched.");
|
|
24091
|
+
return;
|
|
24092
|
+
}
|
|
24093
|
+
if ((bundle.manifest.mode ?? "cms") !== "cms") return;
|
|
24094
|
+
if (!bundle.collectionsDir) return;
|
|
24095
|
+
const primary = dataSources[0];
|
|
24096
|
+
if (!primary?.bootstrapper.ensureCollectionSchema) return;
|
|
24097
|
+
const collections = await loadCollectionsFromDirectory(bundle.collectionsDir);
|
|
24098
|
+
if (collections.length === 0) return;
|
|
24099
|
+
const { applied } = await primary.bootstrapper.ensureCollectionSchema(collections, primary.connection, (message) => logger.info(`schema: ${message}`));
|
|
24100
|
+
logger.info(applied > 0 ? `Applied ${applied} additive schema change(s) before boot.` : "Collection schema is up to date.");
|
|
24101
|
+
}
|
|
23956
24102
|
//#endregion
|
|
23957
24103
|
//#region src/api/contract-routes.ts
|
|
23958
24104
|
var contract_routes_exports = /* @__PURE__ */ __exportAll({ createContractRoutes: () => createContractRoutes });
|