@rebasepro/server 0.9.1-canary.1d2d8b5 → 0.9.1-canary.58368ce
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 +4 -1
- package/dist/index.es.js +354 -31
- package/dist/index.es.js.map +1 -1
- package/dist/init.d.ts +15 -0
- package/dist/{jwt-BwIn8xmk.js → jwt-C6pbGtvf.js} +157 -4
- package/dist/jwt-C6pbGtvf.js.map +1 -0
- package/dist/{src-B4OLmNVa.js → src-CsHhSKbi.js} +60 -2
- package/dist/src-CsHhSKbi.js.map +1 -0
- package/package.json +7 -6
- package/dist/jwt-BwIn8xmk.js.map +0 -1
- package/dist/ms-DnYXB-Wd.js +0 -162
- package/dist/ms-DnYXB-Wd.js.map +0 -1
- package/dist/src-B4OLmNVa.js.map +0 -1
- package/dist/src-B9tjYmqP.js +0 -24598
- package/dist/src-B9tjYmqP.js.map +0 -1
|
@@ -5,12 +5,15 @@ import { HonoEnv } from "../types";
|
|
|
5
5
|
* Lightweight REST API generator that leverages existing Rebase DataDriver.
|
|
6
6
|
* Supports `include` query parameter for eager-loading relations via Drizzle.
|
|
7
7
|
*/
|
|
8
|
+
/** Rows accepted by a single POST /<collection>/bulk. See `maxBulkRows`. */
|
|
9
|
+
export declare const DEFAULT_MAX_BULK_ROWS = 1000;
|
|
8
10
|
export declare class RestApiGenerator {
|
|
9
11
|
private collections;
|
|
10
12
|
private router;
|
|
11
13
|
private driver;
|
|
14
|
+
private maxBulkRows;
|
|
12
15
|
private authAdapter?;
|
|
13
|
-
constructor(collections: CollectionConfig[], driver: DataDriver, authAdapter?: AuthAdapter);
|
|
16
|
+
constructor(collections: CollectionConfig[], driver: DataDriver, authAdapter?: AuthAdapter, maxBulkRows?: number);
|
|
14
17
|
/**
|
|
15
18
|
* Generate REST routes using existing DataDriver
|
|
16
19
|
*/
|
package/dist/index.es.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
import process from "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
|
-
import { a as
|
|
5
|
-
import { _ as
|
|
4
|
+
import { a as getAccessTokenExpiry, d as require_jsonwebtoken, f as __commonJSMin, h as __toESM, i as generateRefreshToken, l as verifyAccessToken, m as __require, n as generateAccessToken, o as getRefreshTokenExpiry, p as __exportAll, r as generateDownloadToken, s as hashRefreshToken, t as configureJwt, u as verifyDownloadToken } from "./jwt-C6pbGtvf.js";
|
|
5
|
+
import { _ as RebaseApiError, a as policy, c as isPostgresCollectionConfig, d as REST_TO_CANONICAL, f as toCanonicalOp, g as Vector, h as GeoPoint, i as isSQLAdmin, l as CANONICAL_TO_REST, m as EntityRelation, o as getCollectionDataPath, p as EntityReference, r as getDataSourceCapabilities, s as getDeclaredSubcollections, t as DEFAULT_STORAGE_SOURCE_KEY, u as NULL_OPS, v as RebaseClientError } from "./src-CsHhSKbi.js";
|
|
6
6
|
import { t as logger } from "./logger-BYU66ENZ.js";
|
|
7
|
-
import { a as getAccessTokenExpiry, d as require_jsonwebtoken, i as generateRefreshToken, l as verifyAccessToken, n as generateAccessToken, o as getRefreshTokenExpiry, r as generateDownloadToken, s as hashRefreshToken, t as configureJwt, u as verifyDownloadToken } from "./jwt-BwIn8xmk.js";
|
|
8
7
|
import { t as nativeDynamicImport } from "./dynamic-import-Dvh-K5fl.js";
|
|
9
8
|
import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
|
|
10
9
|
import * as fs$2 from "fs";
|
|
@@ -1109,6 +1108,76 @@ function normalizeToEntityRelation(value, propertyType) {
|
|
|
1109
1108
|
if (!(obj.__type === "relation" || obj.__type === "reference" || typeof obj.isEntityRelation === "function" && obj.isEntityRelation() || typeof obj.isEntityReference === "function" && obj.isEntityReference() || propertyType === "relation" && typeof obj.id !== "undefined" && typeof obj.path === "string")) return null;
|
|
1110
1109
|
return new EntityRelation(obj.id, obj.path, obj.data);
|
|
1111
1110
|
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Derive a row's address from its key columns.
|
|
1113
|
+
*
|
|
1114
|
+
* Single key → the value as a string. Composite → each part joined by
|
|
1115
|
+
* {@link COMPOSITE_ID_SEPARATOR}, in primary-key order, which is what
|
|
1116
|
+
* {@link parseIdValues} expects to invert.
|
|
1117
|
+
*/
|
|
1118
|
+
function buildCompositeId(values, primaryKeys) {
|
|
1119
|
+
if (primaryKeys.length === 0) return "";
|
|
1120
|
+
if (primaryKeys.length === 1) return String(values[primaryKeys[0].fieldName] ?? "");
|
|
1121
|
+
return primaryKeys.map((pk) => String(values[pk.fieldName] ?? "")).join(":::");
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* The primary keys of a collection, as declared by its properties.
|
|
1125
|
+
*
|
|
1126
|
+
* This is the only tier both sides can read, because it is the only one written
|
|
1127
|
+
* in the config: the postgres driver can also infer keys from the Drizzle
|
|
1128
|
+
* schema, which the browser never sees and is never sent — the admin compiles
|
|
1129
|
+
* the collection files into its own bundle rather than being served them. A key
|
|
1130
|
+
* that lives only in the Drizzle schema is therefore invisible here, and the
|
|
1131
|
+
* server says so at boot (`warnOnKeysTheAdminCannotResolve`) naming the `isId`
|
|
1132
|
+
* to add.
|
|
1133
|
+
*
|
|
1134
|
+
* Returns an empty array when a collection declares none, which callers must
|
|
1135
|
+
* treat as "not addressable" rather than defaulting to `id`: guessing a key
|
|
1136
|
+
* that is not the real one produces confidently wrong addresses.
|
|
1137
|
+
*/
|
|
1138
|
+
function getDeclaredPrimaryKeys(collection) {
|
|
1139
|
+
const properties = collection.properties;
|
|
1140
|
+
if (!properties) return [];
|
|
1141
|
+
const keys = [];
|
|
1142
|
+
for (const [fieldName, propRaw] of Object.entries(properties)) {
|
|
1143
|
+
const prop = propRaw;
|
|
1144
|
+
if (!prop || typeof prop !== "object") continue;
|
|
1145
|
+
if (!("isId" in prop) || !prop.isId) continue;
|
|
1146
|
+
keys.push({
|
|
1147
|
+
fieldName,
|
|
1148
|
+
type: prop.type === "number" ? "number" : "string",
|
|
1149
|
+
isUUID: prop.isId === "uuid"
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
return keys;
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* The keys to address a collection's rows with, resolved the way the driver
|
|
1156
|
+
* resolves them — minus the tier the browser cannot reach.
|
|
1157
|
+
*
|
|
1158
|
+
* The postgres driver tries, in order: properties marked `isId`; the primary
|
|
1159
|
+
* keys of the Drizzle schema; and finally a column literally named `id`. Only
|
|
1160
|
+
* the first and last are visible in a `CollectionConfig`, which is what both
|
|
1161
|
+
* sides share.
|
|
1162
|
+
*
|
|
1163
|
+
* So the two agree except on a collection that declares no `isId` and whose key
|
|
1164
|
+
* is known only to Drizzle. There, the driver reads the real key, and this
|
|
1165
|
+
* either resolves nothing (reported to the console by the caller) or — if the
|
|
1166
|
+
* table happens to have an unrelated `id` property — resolves `id`, which is
|
|
1167
|
+
* the wrong key and cannot be detected from here: the addresses look right and
|
|
1168
|
+
* route wrong. Only the config can settle it, so the server names both cases
|
|
1169
|
+
* at boot (`warnOnKeysTheAdminCannotResolve`) with the `isId` to add.
|
|
1170
|
+
*/
|
|
1171
|
+
function resolvePrimaryKeys(collection) {
|
|
1172
|
+
const declared = getDeclaredPrimaryKeys(collection);
|
|
1173
|
+
if (declared.length > 0) return declared;
|
|
1174
|
+
const idProp = collection.properties?.id;
|
|
1175
|
+
if (idProp && typeof idProp === "object") return [{
|
|
1176
|
+
fieldName: "id",
|
|
1177
|
+
type: idProp.type === "number" ? "number" : "string"
|
|
1178
|
+
}];
|
|
1179
|
+
return [];
|
|
1180
|
+
}
|
|
1112
1181
|
//#endregion
|
|
1113
1182
|
//#region ../common/src/util/enums.ts
|
|
1114
1183
|
function enumToObjectEntries(enumValues) {
|
|
@@ -1321,6 +1390,8 @@ function getSubcollections(collection) {
|
|
|
1321
1390
|
}
|
|
1322
1391
|
return [];
|
|
1323
1392
|
}
|
|
1393
|
+
policy.or(policy.serverContext(), policy.rolesOverlap(["admin"]));
|
|
1394
|
+
policy.or(policy.serverContext(), policy.rolesOverlap(["admin"]));
|
|
1324
1395
|
(/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
1325
1396
|
(function(root, factory) {
|
|
1326
1397
|
if (typeof define === "function" && define.amd) define(factory);
|
|
@@ -2753,18 +2824,45 @@ function deserializeLogicalCondition(str) {
|
|
|
2753
2824
|
}
|
|
2754
2825
|
//#endregion
|
|
2755
2826
|
//#region ../common/src/data/buildRebaseData.ts
|
|
2827
|
+
function createPrimaryKeyResolver(options) {
|
|
2828
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2829
|
+
const warned = /* @__PURE__ */ new Set();
|
|
2830
|
+
return function primaryKeysFor(slug) {
|
|
2831
|
+
const cached = cache.get(slug);
|
|
2832
|
+
if (cached) return cached;
|
|
2833
|
+
const collection = options?.resolveCollection?.(slug);
|
|
2834
|
+
if (!collection) return [];
|
|
2835
|
+
const keys = resolvePrimaryKeys(collection);
|
|
2836
|
+
if (keys.length > 0) {
|
|
2837
|
+
cache.set(slug, keys);
|
|
2838
|
+
return keys;
|
|
2839
|
+
}
|
|
2840
|
+
if (!warned.has(slug)) {
|
|
2841
|
+
warned.add(slug);
|
|
2842
|
+
console.warn(`[rebase] Collection '${slug}' declares no primary key, so its rows have no address: detail links, caching and relations will not work for it. Mark the key property with \`isId\` in its collection config — the server logs which column to mark at boot, if its schema knows the key.`);
|
|
2843
|
+
}
|
|
2844
|
+
return keys;
|
|
2845
|
+
};
|
|
2846
|
+
}
|
|
2756
2847
|
/**
|
|
2757
|
-
*
|
|
2758
|
-
*
|
|
2848
|
+
* Give a flat row the Entity view-model the admin renders.
|
|
2849
|
+
*
|
|
2850
|
+
* The address is *derived here* — it is not a column, and the row it came from
|
|
2851
|
+
* does not contain one. Rows carry exactly what the table has, with the types
|
|
2852
|
+
* Postgres returned; the id is this layer's invention, and this is the only
|
|
2853
|
+
* place it is minted.
|
|
2854
|
+
*
|
|
2855
|
+
* `primaryKeys` empty falls back to a literal `id` on the row: drivers other
|
|
2856
|
+
* than postgres still serve rows with one, and this keeps them working.
|
|
2759
2857
|
*/
|
|
2760
|
-
function rowToEntity(row, slug) {
|
|
2858
|
+
function rowToEntity(row, slug, primaryKeys = []) {
|
|
2761
2859
|
return {
|
|
2762
|
-
id: row.id,
|
|
2860
|
+
id: primaryKeys.length > 0 ? buildCompositeId(row, primaryKeys) : row.id,
|
|
2763
2861
|
path: slug,
|
|
2764
2862
|
values: row
|
|
2765
2863
|
};
|
|
2766
2864
|
}
|
|
2767
|
-
function createDriverAccessor(driver, slug) {
|
|
2865
|
+
function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
2768
2866
|
const accessor = {
|
|
2769
2867
|
async find(params) {
|
|
2770
2868
|
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
@@ -2797,7 +2895,7 @@ function createDriverAccessor(driver, slug) {
|
|
|
2797
2895
|
hasMore = offset + rows.length < total;
|
|
2798
2896
|
}
|
|
2799
2897
|
return {
|
|
2800
|
-
data: rows.map((row) => rowToEntity(row, slug)),
|
|
2898
|
+
data: rows.map((row) => rowToEntity(row, slug, getPks())),
|
|
2801
2899
|
meta: {
|
|
2802
2900
|
total,
|
|
2803
2901
|
limit,
|
|
@@ -2811,7 +2909,7 @@ function createDriverAccessor(driver, slug) {
|
|
|
2811
2909
|
path: slug,
|
|
2812
2910
|
id
|
|
2813
2911
|
});
|
|
2814
|
-
return row ? rowToEntity(row, slug) : void 0;
|
|
2912
|
+
return row ? rowToEntity(row, slug, getPks()) : void 0;
|
|
2815
2913
|
},
|
|
2816
2914
|
async create(data, id) {
|
|
2817
2915
|
return rowToEntity(await driver.save({
|
|
@@ -2819,15 +2917,22 @@ function createDriverAccessor(driver, slug) {
|
|
|
2819
2917
|
values: data,
|
|
2820
2918
|
id,
|
|
2821
2919
|
status: "new"
|
|
2822
|
-
}), slug);
|
|
2920
|
+
}), slug, getPks());
|
|
2823
2921
|
},
|
|
2922
|
+
createMany: driver.saveMany ? async (data, options) => {
|
|
2923
|
+
return (await driver.saveMany({
|
|
2924
|
+
path: slug,
|
|
2925
|
+
rows: data,
|
|
2926
|
+
upsert: options?.upsert
|
|
2927
|
+
})).map((row) => rowToEntity(row, slug, getPks()));
|
|
2928
|
+
} : void 0,
|
|
2824
2929
|
async update(id, data) {
|
|
2825
2930
|
return rowToEntity(await driver.save({
|
|
2826
2931
|
path: slug,
|
|
2827
2932
|
values: data,
|
|
2828
2933
|
id,
|
|
2829
2934
|
status: "existing"
|
|
2830
|
-
}), slug);
|
|
2935
|
+
}), slug, getPks());
|
|
2831
2936
|
},
|
|
2832
2937
|
async delete(id) {
|
|
2833
2938
|
return driver.delete({ row: {
|
|
@@ -2856,7 +2961,7 @@ function createDriverAccessor(driver, slug) {
|
|
|
2856
2961
|
searchString: params?.searchString,
|
|
2857
2962
|
onUpdate: (entities) => {
|
|
2858
2963
|
onUpdate({
|
|
2859
|
-
data: entities.map((row) => rowToEntity(row, slug)),
|
|
2964
|
+
data: entities.map((row) => rowToEntity(row, slug, getPks())),
|
|
2860
2965
|
meta: {
|
|
2861
2966
|
total: entities.length,
|
|
2862
2967
|
limit,
|
|
@@ -2872,7 +2977,7 @@ function createDriverAccessor(driver, slug) {
|
|
|
2872
2977
|
return driver.listenOne({
|
|
2873
2978
|
path: slug,
|
|
2874
2979
|
id,
|
|
2875
|
-
onUpdate: (entity) => onUpdate(entity ? rowToEntity(entity, slug) : void 0),
|
|
2980
|
+
onUpdate: (entity) => onUpdate(entity ? rowToEntity(entity, slug, getPks()) : void 0),
|
|
2876
2981
|
onError
|
|
2877
2982
|
});
|
|
2878
2983
|
} : void 0,
|
|
@@ -2911,12 +3016,13 @@ function createDriverAccessor(driver, slug) {
|
|
|
2911
3016
|
* await data.products.create({ name: "Camera", price: 299 });
|
|
2912
3017
|
* const { data: items } = await data.products.find({ where: { status: ["==", "published"] } });
|
|
2913
3018
|
*/
|
|
2914
|
-
function buildRebaseData(driver) {
|
|
3019
|
+
function buildRebaseData(driver, options) {
|
|
2915
3020
|
const cache = /* @__PURE__ */ new Map();
|
|
3021
|
+
const primaryKeysFor = createPrimaryKeyResolver(options);
|
|
2916
3022
|
function getAccessor(slug) {
|
|
2917
3023
|
let accessor = cache.get(slug);
|
|
2918
3024
|
if (!accessor) {
|
|
2919
|
-
accessor = createDriverAccessor(driver, slug);
|
|
3025
|
+
accessor = createDriverAccessor(driver, slug, () => primaryKeysFor(slug));
|
|
2920
3026
|
cache.set(slug, accessor);
|
|
2921
3027
|
}
|
|
2922
3028
|
return accessor;
|
|
@@ -2929,8 +3035,9 @@ function buildRebaseData(driver) {
|
|
|
2929
3035
|
} });
|
|
2930
3036
|
}
|
|
2931
3037
|
/**
|
|
2932
|
-
* Unwrap a Entity into
|
|
2933
|
-
*
|
|
3038
|
+
* Unwrap a Entity back into the flat row it was built from. `rowToEntity` keeps
|
|
3039
|
+
* the row untouched under `.values` and derives `.id` alongside it, so dropping
|
|
3040
|
+
* the wrapper is the whole operation — the address was never part of the row.
|
|
2934
3041
|
*/
|
|
2935
3042
|
function entityToRow(entity) {
|
|
2936
3043
|
return entity.values;
|
|
@@ -3017,6 +3124,12 @@ function toSdkCollectionClient(snap) {
|
|
|
3017
3124
|
async create(data, id) {
|
|
3018
3125
|
return entityToRow(await snap.create(data, id));
|
|
3019
3126
|
},
|
|
3127
|
+
async createMany(data, options) {
|
|
3128
|
+
if (!Array.isArray(data)) throw new TypeError("createMany expects an array of records.");
|
|
3129
|
+
if (data.length === 0) return [];
|
|
3130
|
+
if (!snap.createMany) throw new Error("Bulk writes are not supported by this collection's data source. Fall back to create() per record.");
|
|
3131
|
+
return (await snap.createMany(data, options)).map(entityToRow);
|
|
3132
|
+
},
|
|
3020
3133
|
async update(id, data) {
|
|
3021
3134
|
return entityToRow(await snap.update(id, data));
|
|
3022
3135
|
},
|
|
@@ -3725,15 +3838,19 @@ async function parseJsonBody(c) {
|
|
|
3725
3838
|
* Lightweight REST API generator that leverages existing Rebase DataDriver.
|
|
3726
3839
|
* Supports `include` query parameter for eager-loading relations via Drizzle.
|
|
3727
3840
|
*/
|
|
3841
|
+
/** Rows accepted by a single POST /<collection>/bulk. See `maxBulkRows`. */
|
|
3842
|
+
var DEFAULT_MAX_BULK_ROWS = 1e3;
|
|
3728
3843
|
var RestApiGenerator = class {
|
|
3729
3844
|
collections;
|
|
3730
3845
|
router;
|
|
3731
3846
|
driver;
|
|
3847
|
+
maxBulkRows;
|
|
3732
3848
|
authAdapter;
|
|
3733
|
-
constructor(collections, driver, authAdapter) {
|
|
3849
|
+
constructor(collections, driver, authAdapter, maxBulkRows = DEFAULT_MAX_BULK_ROWS) {
|
|
3734
3850
|
this.collections = collections;
|
|
3735
3851
|
this.driver = driver;
|
|
3736
3852
|
this.authAdapter = authAdapter;
|
|
3853
|
+
this.maxBulkRows = maxBulkRows;
|
|
3737
3854
|
this.router = new Hono();
|
|
3738
3855
|
}
|
|
3739
3856
|
/**
|
|
@@ -3818,6 +3935,32 @@ var RestApiGenerator = class {
|
|
|
3818
3935
|
if (!entity) throw ApiError.notFound("Entity not found");
|
|
3819
3936
|
return c.json(entity);
|
|
3820
3937
|
});
|
|
3938
|
+
this.router.post(`${basePath}/bulk`, async (c) => {
|
|
3939
|
+
this.enforceApiKeyPermission(c, collection.slug);
|
|
3940
|
+
const driver = this.getScopedDriver(c);
|
|
3941
|
+
const path = collection.slug;
|
|
3942
|
+
const body = await parseJsonBody(c);
|
|
3943
|
+
if (!Array.isArray(body?.rows)) throw ApiError.badRequest("Expected a JSON body of { rows: [...] }.", "INVALID_BULK_BODY");
|
|
3944
|
+
if (body.rows.length === 0) return c.json({
|
|
3945
|
+
data: [],
|
|
3946
|
+
meta: { written: 0 }
|
|
3947
|
+
});
|
|
3948
|
+
if (body.rows.some((row) => typeof row !== "object" || row === null || Array.isArray(row))) throw ApiError.badRequest("Every entry in `rows` must be an object.", "INVALID_BULK_BODY");
|
|
3949
|
+
if (body.upsert !== void 0 && typeof body.upsert !== "boolean") throw ApiError.badRequest("`upsert` must be a boolean.", "INVALID_BULK_BODY");
|
|
3950
|
+
const maxRows = this.maxBulkRows;
|
|
3951
|
+
if (body.rows.length > maxRows) throw ApiError.badRequest(`Too many rows: ${body.rows.length} exceeds the ${maxRows}-row limit for a single bulk write. Send it in chunks of ${maxRows} or fewer.`, "BULK_TOO_LARGE");
|
|
3952
|
+
if (!driver.saveMany) throw ApiError.badRequest("This collection's data source does not support bulk writes.", "BULK_UNSUPPORTED");
|
|
3953
|
+
const rows = await driver.saveMany({
|
|
3954
|
+
path,
|
|
3955
|
+
rows: body.rows,
|
|
3956
|
+
collection: resolvedCollection,
|
|
3957
|
+
upsert: body.upsert === true
|
|
3958
|
+
});
|
|
3959
|
+
return c.json({
|
|
3960
|
+
data: rows.map((row) => this.formatResponse(row)),
|
|
3961
|
+
meta: { written: rows.length }
|
|
3962
|
+
});
|
|
3963
|
+
});
|
|
3821
3964
|
this.router.post(basePath, async (c) => {
|
|
3822
3965
|
try {
|
|
3823
3966
|
this.enforceApiKeyPermission(c, collection.slug);
|
|
@@ -9549,7 +9692,7 @@ function mountSessionRoutes(opts) {
|
|
|
9549
9692
|
clearRefreshCookie(c, config.cookieAuth);
|
|
9550
9693
|
const authHeader = c.req.header("authorization");
|
|
9551
9694
|
if (ops.afterLogout && authHeader?.startsWith("Bearer ")) {
|
|
9552
|
-
const { verifyAccessToken } = await import("./jwt-
|
|
9695
|
+
const { verifyAccessToken } = await import("./jwt-C6pbGtvf.js").then((n) => n.c);
|
|
9553
9696
|
const payload = verifyAccessToken(authHeader.substring(7));
|
|
9554
9697
|
if (payload) ops.afterLogout(payload.userId).catch((err) => {
|
|
9555
9698
|
logger.error("[AuthHooks] afterLogout error", { error: err instanceof Error ? err.message : err });
|
|
@@ -10953,6 +11096,98 @@ function requestLogger(options) {
|
|
|
10953
11096
|
};
|
|
10954
11097
|
}
|
|
10955
11098
|
//#endregion
|
|
11099
|
+
//#region src/api/logs-routes.ts
|
|
11100
|
+
var logs_routes_exports = /* @__PURE__ */ __exportAll({
|
|
11101
|
+
addLog: () => addLog,
|
|
11102
|
+
default: () => app,
|
|
11103
|
+
logBuffer: () => logBuffer,
|
|
11104
|
+
logMiddleware: () => logMiddleware
|
|
11105
|
+
});
|
|
11106
|
+
var LogRingBuffer = class {
|
|
11107
|
+
buffer = [];
|
|
11108
|
+
maxSize;
|
|
11109
|
+
idCounter = 0;
|
|
11110
|
+
constructor(maxSize = 1e4) {
|
|
11111
|
+
this.maxSize = maxSize;
|
|
11112
|
+
}
|
|
11113
|
+
push(entry) {
|
|
11114
|
+
const id = `log_${++this.idCounter}`;
|
|
11115
|
+
this.buffer.push({
|
|
11116
|
+
...entry,
|
|
11117
|
+
id
|
|
11118
|
+
});
|
|
11119
|
+
if (this.buffer.length > this.maxSize) this.buffer.shift();
|
|
11120
|
+
}
|
|
11121
|
+
query(options) {
|
|
11122
|
+
let filtered = this.buffer;
|
|
11123
|
+
if (options.level) filtered = filtered.filter((e) => e.level === options.level);
|
|
11124
|
+
if (options.source) filtered = filtered.filter((e) => e.source === options.source);
|
|
11125
|
+
if (options.search) {
|
|
11126
|
+
const searchLower = options.search.toLowerCase();
|
|
11127
|
+
filtered = filtered.filter((e) => e.message.toLowerCase().includes(searchLower));
|
|
11128
|
+
}
|
|
11129
|
+
if (options.since) {
|
|
11130
|
+
const sinceValue = options.since;
|
|
11131
|
+
filtered = filtered.filter((e) => e.timestamp >= sinceValue);
|
|
11132
|
+
}
|
|
11133
|
+
const sorted = [...filtered].reverse();
|
|
11134
|
+
const total = sorted.length;
|
|
11135
|
+
const limit = options.limit || 100;
|
|
11136
|
+
const offset = options.offset || 0;
|
|
11137
|
+
return {
|
|
11138
|
+
entries: sorted.slice(offset, offset + limit),
|
|
11139
|
+
total
|
|
11140
|
+
};
|
|
11141
|
+
}
|
|
11142
|
+
getLatest(count = 50) {
|
|
11143
|
+
return this.buffer.slice(-count).reverse();
|
|
11144
|
+
}
|
|
11145
|
+
};
|
|
11146
|
+
var logBuffer = new LogRingBuffer();
|
|
11147
|
+
/** Add a log entry */
|
|
11148
|
+
function addLog(level, source, message, metadata) {
|
|
11149
|
+
logBuffer.push({
|
|
11150
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11151
|
+
level,
|
|
11152
|
+
source,
|
|
11153
|
+
message,
|
|
11154
|
+
metadata
|
|
11155
|
+
});
|
|
11156
|
+
}
|
|
11157
|
+
/** Hono middleware to log API requests */
|
|
11158
|
+
function logMiddleware() {
|
|
11159
|
+
return async (c, next) => {
|
|
11160
|
+
const start = Date.now();
|
|
11161
|
+
await next();
|
|
11162
|
+
const duration = Date.now() - start;
|
|
11163
|
+
const reqId = c.get("requestId");
|
|
11164
|
+
addLog("info", "api", `${c.req.method} ${c.req.path} ${c.res.status} ${duration}ms`, {
|
|
11165
|
+
method: c.req.method,
|
|
11166
|
+
path: c.req.path,
|
|
11167
|
+
status: c.res.status,
|
|
11168
|
+
duration,
|
|
11169
|
+
...reqId && { requestId: reqId }
|
|
11170
|
+
});
|
|
11171
|
+
};
|
|
11172
|
+
}
|
|
11173
|
+
var app = new Hono();
|
|
11174
|
+
app.get("/", (c) => {
|
|
11175
|
+
const query = c.req.query();
|
|
11176
|
+
const result = logBuffer.query({
|
|
11177
|
+
level: query.level,
|
|
11178
|
+
source: query.source,
|
|
11179
|
+
search: query.search,
|
|
11180
|
+
limit: query.limit ? parseInt(query.limit) : void 0,
|
|
11181
|
+
offset: query.offset ? parseInt(query.offset) : void 0,
|
|
11182
|
+
since: query.since
|
|
11183
|
+
});
|
|
11184
|
+
return c.json(result);
|
|
11185
|
+
});
|
|
11186
|
+
app.get("/latest", (c) => {
|
|
11187
|
+
const count = parseInt(c.req.query("count") || "50");
|
|
11188
|
+
return c.json({ entries: logBuffer.getLatest(count) });
|
|
11189
|
+
});
|
|
11190
|
+
//#endregion
|
|
10956
11191
|
//#region src/init/middlewares.ts
|
|
10957
11192
|
function configureMiddlewares(app, basePath, isProduction, config) {
|
|
10958
11193
|
app.use(`${basePath}/*`, requestId());
|
|
@@ -10979,6 +11214,7 @@ function configureMiddlewares(app, basePath, isProduction, config) {
|
|
|
10979
11214
|
}
|
|
10980
11215
|
if (!isProduction && !process.env.CORS_ORIGINS && !process.env.FRONTEND_URL) logger.warn("No CORS configuration detected (CORS_ORIGINS / FRONTEND_URL not set). The API will accept requests from any origin. Set CORS_ORIGINS in your .env file to restrict access.");
|
|
10981
11216
|
app.use(`${basePath}/*`, requestLogger());
|
|
11217
|
+
app.use(`${basePath}/*`, logMiddleware());
|
|
10982
11218
|
}
|
|
10983
11219
|
//#endregion
|
|
10984
11220
|
//#region src/storage/types.ts
|
|
@@ -11518,7 +11754,7 @@ var gcsModule = null;
|
|
|
11518
11754
|
async function loadGCS() {
|
|
11519
11755
|
if (gcsModule) return gcsModule;
|
|
11520
11756
|
try {
|
|
11521
|
-
gcsModule = await import("
|
|
11757
|
+
gcsModule = await import("@google-cloud/storage");
|
|
11522
11758
|
return gcsModule;
|
|
11523
11759
|
} catch {
|
|
11524
11760
|
throw new Error("Google Cloud Storage is required for the GCS storage controller. Install it with: pnpm add @google-cloud/storage");
|
|
@@ -14965,7 +15201,7 @@ var SDKQueryBuilder = class {
|
|
|
14965
15201
|
* Listen to realtime updates matching this query.
|
|
14966
15202
|
*/
|
|
14967
15203
|
listen(onUpdate, onError) {
|
|
14968
|
-
if (!this.collection.listen) throw new Error("Listen is only available when RebaseClient is configured with a websocketUrl.");
|
|
15204
|
+
if (!this.collection.listen) throw new Error("Listen is only available when RebaseClient is configured with a websocketUrl, and not when it was created with realtime: false.");
|
|
14969
15205
|
return this.collection.listen(this.params, onUpdate, onError);
|
|
14970
15206
|
}
|
|
14971
15207
|
};
|
|
@@ -14998,6 +15234,17 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
14998
15234
|
body: JSON.stringify(body)
|
|
14999
15235
|
});
|
|
15000
15236
|
},
|
|
15237
|
+
async createMany(data, options) {
|
|
15238
|
+
if (!Array.isArray(data)) throw new TypeError("createMany expects an array of records.");
|
|
15239
|
+
if (data.length === 0) return [];
|
|
15240
|
+
return (await transport.request(`${basePath}/bulk`, {
|
|
15241
|
+
method: "POST",
|
|
15242
|
+
body: JSON.stringify({
|
|
15243
|
+
rows: data,
|
|
15244
|
+
...options?.upsert ? { upsert: true } : {}
|
|
15245
|
+
})
|
|
15246
|
+
})).data || [];
|
|
15247
|
+
},
|
|
15001
15248
|
async update(id, data) {
|
|
15002
15249
|
return await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, {
|
|
15003
15250
|
method: "PUT",
|
|
@@ -15605,7 +15852,9 @@ var RebaseWebSocketClient = class {
|
|
|
15605
15852
|
const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
|
|
15606
15853
|
if (collectionSub) {
|
|
15607
15854
|
const incomingRows = message.rows || [];
|
|
15608
|
-
const
|
|
15855
|
+
const updatePks = message.pks;
|
|
15856
|
+
if (updatePks) collectionSub.pks = updatePks;
|
|
15857
|
+
const rows = this.mergeRows(collectionSub.latestData, incomingRows, collectionSub.pks);
|
|
15609
15858
|
collectionSub.latestData = rows;
|
|
15610
15859
|
collectionSub.lastUpdated = Date.now();
|
|
15611
15860
|
collectionSub.isInitialDataReceived = true;
|
|
@@ -15627,12 +15876,14 @@ var RebaseWebSocketClient = class {
|
|
|
15627
15876
|
const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
|
|
15628
15877
|
if (collectionSub && collectionSub.isInitialDataReceived && collectionSub.latestData) {
|
|
15629
15878
|
const patchWireEntity = message.row ?? null;
|
|
15630
|
-
const
|
|
15879
|
+
const patchMessage = message;
|
|
15880
|
+
const patchEntityId = patchMessage.id;
|
|
15881
|
+
if (patchMessage.pks) collectionSub.pks = patchMessage.pks;
|
|
15631
15882
|
const patchRow = patchWireEntity ? patchWireEntity : null;
|
|
15632
15883
|
let updated;
|
|
15633
|
-
if (patchRow === null) updated = collectionSub.latestData.filter((e) =>
|
|
15884
|
+
if (patchRow === null) updated = collectionSub.latestData.filter((e) => this.rowAddress(e, collectionSub.pks) !== String(patchEntityId));
|
|
15634
15885
|
else {
|
|
15635
|
-
const idx = collectionSub.latestData.findIndex((e) =>
|
|
15886
|
+
const idx = collectionSub.latestData.findIndex((e) => this.rowAddress(e, collectionSub.pks) === String(patchEntityId));
|
|
15636
15887
|
if (idx >= 0) {
|
|
15637
15888
|
updated = [...collectionSub.latestData];
|
|
15638
15889
|
updated[idx] = patchRow;
|
|
@@ -15982,17 +16233,39 @@ var RebaseWebSocketClient = class {
|
|
|
15982
16233
|
return val;
|
|
15983
16234
|
}
|
|
15984
16235
|
/**
|
|
16236
|
+
* The address of a row, for matching it against another copy of itself.
|
|
16237
|
+
*
|
|
16238
|
+
* A row is exactly its columns and carries no address, so it is derived
|
|
16239
|
+
* from the key columns the server named — including the ordinary case where
|
|
16240
|
+
* that key is `id`, which the server reports like any other.
|
|
16241
|
+
*
|
|
16242
|
+
* Undefined when there are no keys, which means the server could not
|
|
16243
|
+
* resolve any: such rows genuinely cannot be recognised, and guessing at a
|
|
16244
|
+
* column called `id` would be inventing an identity for a table that has
|
|
16245
|
+
* none.
|
|
16246
|
+
*/
|
|
16247
|
+
rowAddress(row, pks) {
|
|
16248
|
+
if (!pks || pks.length === 0) return void 0;
|
|
16249
|
+
const address = buildCompositeId(row, pks);
|
|
16250
|
+
if (!address || address.split(":::").every((part) => part === "")) return void 0;
|
|
16251
|
+
return address;
|
|
16252
|
+
}
|
|
16253
|
+
/**
|
|
15985
16254
|
* Merge incoming rows with cached data, preserving cached references
|
|
15986
16255
|
* for rows whose values haven't changed. This avoids unnecessary
|
|
15987
16256
|
* React re-renders when the server refetches all rows but most
|
|
15988
16257
|
* haven't actually changed.
|
|
15989
16258
|
*/
|
|
15990
|
-
mergeRows(cached, incoming) {
|
|
16259
|
+
mergeRows(cached, incoming, pks) {
|
|
15991
16260
|
if (!cached || cached.length === 0) return incoming;
|
|
15992
16261
|
const cachedById = /* @__PURE__ */ new Map();
|
|
15993
|
-
for (const row of cached)
|
|
16262
|
+
for (const row of cached) {
|
|
16263
|
+
const address = this.rowAddress(row, pks);
|
|
16264
|
+
if (address !== void 0) cachedById.set(address, row);
|
|
16265
|
+
}
|
|
15994
16266
|
return incoming.map((incomingRow) => {
|
|
15995
|
-
const
|
|
16267
|
+
const address = this.rowAddress(incomingRow, pks);
|
|
16268
|
+
const cachedRow = address === void 0 ? void 0 : cachedById.get(address);
|
|
15996
16269
|
if (!cachedRow) return incomingRow;
|
|
15997
16270
|
const normCached = this.normalizeForComparison(cachedRow);
|
|
15998
16271
|
const normIncoming = this.normalizeForComparison(incomingRow);
|
|
@@ -16004,7 +16277,7 @@ var RebaseWebSocketClient = class {
|
|
|
16004
16277
|
cached: normCached[key],
|
|
16005
16278
|
incoming: normIncoming[key]
|
|
16006
16279
|
};
|
|
16007
|
-
console.debug(`[RebaseWS] Row ${
|
|
16280
|
+
console.debug(`[RebaseWS] Row ${address} refetch mismatch:\n`, JSON.stringify(mismatches, null, 2));
|
|
16008
16281
|
}
|
|
16009
16282
|
return incomingRow;
|
|
16010
16283
|
});
|
|
@@ -16249,7 +16522,7 @@ function createRebaseClient(options) {
|
|
|
16249
16522
|
});
|
|
16250
16523
|
return storageSourcesPromise;
|
|
16251
16524
|
};
|
|
16252
|
-
const resolvedWsUrl = options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl);
|
|
16525
|
+
const resolvedWsUrl = options.realtime !== false ? options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl) : void 0;
|
|
16253
16526
|
let ws;
|
|
16254
16527
|
if (resolvedWsUrl) {
|
|
16255
16528
|
ws = new RebaseWebSocketClient({
|
|
@@ -16361,6 +16634,16 @@ function createRebaseClient(options) {
|
|
|
16361
16634
|
createStorageSource,
|
|
16362
16635
|
fetchStorageSources,
|
|
16363
16636
|
ws,
|
|
16637
|
+
/**
|
|
16638
|
+
* Release the realtime socket and its reconnect timer.
|
|
16639
|
+
*
|
|
16640
|
+
* Until this returns, the open socket keeps the Node event loop alive
|
|
16641
|
+
* and the process will not exit on its own. Safe to call when realtime
|
|
16642
|
+
* was never started, and safe to call twice.
|
|
16643
|
+
*/
|
|
16644
|
+
close: () => {
|
|
16645
|
+
ws?.disconnect();
|
|
16646
|
+
},
|
|
16364
16647
|
setToken: transport.setToken,
|
|
16365
16648
|
setAuthTokenGetter: transport.setAuthTokenGetter,
|
|
16366
16649
|
setOnUnauthorized: transport.setOnUnauthorized,
|
|
@@ -17069,6 +17352,10 @@ async function _initializeRebaseBackend(config) {
|
|
|
17069
17352
|
dataAsAdmin: serverData
|
|
17070
17353
|
});
|
|
17071
17354
|
logger.info("Native data plane attached to singleton (bypasses HTTP loop)");
|
|
17355
|
+
if (storageController) {
|
|
17356
|
+
Object.assign(serverClient, { storage: storageController });
|
|
17357
|
+
logger.info("Native storage attached to singleton (bypasses HTTP loop)");
|
|
17358
|
+
}
|
|
17072
17359
|
let emailService;
|
|
17073
17360
|
if (authConfigResult?.emailService) emailService = authConfigResult.emailService;
|
|
17074
17361
|
else if (config.auth && !isAuthAdapter(config.auth) && config.auth.email) emailService = createEmailService(config.auth.email);
|
|
@@ -17169,6 +17456,17 @@ async function _initializeRebaseBackend(config) {
|
|
|
17169
17456
|
config.app.route(`${basePath}/admin/backups`, backupRouter);
|
|
17170
17457
|
logger.info("Backup admin routes mounted", { path: `${basePath}/admin/backups` });
|
|
17171
17458
|
}
|
|
17459
|
+
{
|
|
17460
|
+
const { default: logsRoutes } = await Promise.resolve().then(() => logs_routes_exports);
|
|
17461
|
+
const logsRouter = new Hono();
|
|
17462
|
+
if (authAdapter && !isAuthAdapter(config.auth)) {
|
|
17463
|
+
const safeAuth = config.auth;
|
|
17464
|
+
if (safeAuth.requireAuth !== false && !!safeAuth.jwtSecret) logsRouter.use("/*", requireAuth, requireAdmin);
|
|
17465
|
+
} else if (authAdapter) logsRouter.use("/*", requireAuth, requireAdmin);
|
|
17466
|
+
logsRouter.route("/", logsRoutes);
|
|
17467
|
+
config.app.route(`${basePath}/logs`, logsRouter);
|
|
17468
|
+
logger.info("Logs routes mounted", { path: `${basePath}/logs` });
|
|
17469
|
+
}
|
|
17172
17470
|
const effectiveRealtimeService = Object.keys(realtimeServices).length > 1 ? createRoutedRealtimeService({
|
|
17173
17471
|
providers: realtimeServices,
|
|
17174
17472
|
defaultKey: defaultDriverId,
|
|
@@ -17182,9 +17480,34 @@ async function _initializeRebaseBackend(config) {
|
|
|
17182
17480
|
cronScheduler,
|
|
17183
17481
|
realtimeServices
|
|
17184
17482
|
});
|
|
17483
|
+
/**
|
|
17484
|
+
* Every registry a driver might resolve callbacks from, plus the backend's
|
|
17485
|
+
* own. Each holds its own normalized copy of a collection, so callbacks have
|
|
17486
|
+
* to be written to all of them.
|
|
17487
|
+
*/
|
|
17488
|
+
const callbackTargets = () => {
|
|
17489
|
+
const targets = [collectionRegistry];
|
|
17490
|
+
for (const key of [DEFAULT_DRIVER_ID, ...driverRegistry.list()]) {
|
|
17491
|
+
const d = driverRegistry.get(key);
|
|
17492
|
+
if (d?.registry && typeof d.registry.get === "function" && !targets.includes(d.registry)) targets.push(d.registry);
|
|
17493
|
+
}
|
|
17494
|
+
return targets;
|
|
17495
|
+
};
|
|
17496
|
+
const setCollectionCallbacks = (slug, callbacks) => {
|
|
17497
|
+
let attached = 0;
|
|
17498
|
+
for (const registry of callbackTargets()) {
|
|
17499
|
+
const collection = registry.get(slug);
|
|
17500
|
+
if (collection) {
|
|
17501
|
+
collection.callbacks = callbacks;
|
|
17502
|
+
attached++;
|
|
17503
|
+
}
|
|
17504
|
+
}
|
|
17505
|
+
if (attached === 0) logger.warn(`[callbacks] Collection "${slug}" not found in any registry — callbacks not attached.`);
|
|
17506
|
+
};
|
|
17185
17507
|
return {
|
|
17186
17508
|
driverRegistry,
|
|
17187
17509
|
driver: defaultDriver,
|
|
17510
|
+
setCollectionCallbacks,
|
|
17188
17511
|
realtimeServices,
|
|
17189
17512
|
realtimeService: effectiveRealtimeService,
|
|
17190
17513
|
auth: authConfigResult,
|