@rebasepro/server 0.13.0 → 0.13.1-canary.g18cfeb7
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/{admin_block-CcSGdH7M.js → admin_block-CLeUMG8G.js} +3 -1
- package/dist/admin_block-CLeUMG8G.js.map +1 -0
- package/dist/api/errors.d.ts +0 -6
- package/dist/api/openapi-generator.d.ts +14 -0
- package/dist/api/rest/idempotency.d.ts +41 -4
- package/dist/api/rest/write-validation.d.ts +12 -5
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/jwt.d.ts +12 -0
- package/dist/{auth-CuC9M2x6.js → auth-CtxXQYV_.js} +20 -39
- package/dist/auth-CtxXQYV_.js.map +1 -0
- package/dist/{backup-CVggVhR2.js → backup-BJiY_mM-.js} +2 -2
- package/dist/{backup-CVggVhR2.js.map → backup-BJiY_mM-.js.map} +1 -1
- package/dist/boot/boot.d.ts +19 -0
- package/dist/{contract-routes-Dj8i5AiM.js → contract-routes-FSt0Nyfk.js} +2 -2
- package/dist/{contract-routes-Dj8i5AiM.js.map → contract-routes-FSt0Nyfk.js.map} +1 -1
- package/dist/cron/cron-scheduler.d.ts +8 -3
- package/dist/cron/define-cron.d.ts +5 -3
- package/dist/{cron-routes-CrQ0tK-_.js → cron-routes-D5a9v1HY.js} +2 -2
- package/dist/{cron-routes-CrQ0tK-_.js.map → cron-routes-D5a9v1HY.js.map} +1 -1
- package/dist/{cron-scheduler-B3RFt0HS.js → cron-scheduler-CsQtYLMq.js} +28 -3
- package/dist/cron-scheduler-CsQtYLMq.js.map +1 -0
- package/dist/{cron-store-BywZsyfZ.js → cron-store-CHH7zrif.js} +16 -3
- package/dist/cron-store-CHH7zrif.js.map +1 -0
- package/dist/{errors-CgkCzoj7.js → errors-BpudWAVU.js} +2 -10
- package/dist/errors-BpudWAVU.js.map +1 -0
- package/dist/functions/define-function.d.ts +1 -1
- package/dist/index.es.js +673 -232
- package/dist/index.es.js.map +1 -1
- package/dist/internal-tables-57VaN3o1.js +105 -0
- package/dist/internal-tables-57VaN3o1.js.map +1 -0
- package/dist/{jwt-DD6EtpGj.js → jwt-CzeviDcB.js} +17 -2
- package/dist/{jwt-DD6EtpGj.js.map → jwt-CzeviDcB.js.map} +1 -1
- package/dist/{openapi-generator-BEwyiaAr.js → openapi-generator-CSlEZh7-.js} +306 -128
- package/dist/openapi-generator-CSlEZh7-.js.map +1 -0
- package/dist/{schema-editor-routes-CbF20Mf1.js → schema-editor-routes-BqjzvvWU.js} +4 -4
- package/dist/{schema-editor-routes-CbF20Mf1.js.map → schema-editor-routes-BqjzvvWU.js.map} +1 -1
- package/dist/{src-_qQ3RNCK.js → src-Ca6NhxKs.js} +79 -2
- package/dist/src-Ca6NhxKs.js.map +1 -0
- package/dist/{src-Cum9kox5.js → src-CvaxLgnM.js} +115 -34
- package/dist/src-CvaxLgnM.js.map +1 -0
- package/dist/utils/logging.d.ts +0 -4
- package/dist/utils/sql.d.ts +11 -6
- package/package.json +6 -6
- package/dist/admin_block-CcSGdH7M.js.map +0 -1
- package/dist/auth-CuC9M2x6.js.map +0 -1
- package/dist/backend-CIxN4FVm.js +0 -15
- package/dist/backend-CIxN4FVm.js.map +0 -1
- package/dist/cron-scheduler-B3RFt0HS.js.map +0 -1
- package/dist/cron-store-BywZsyfZ.js.map +0 -1
- package/dist/errors-CgkCzoj7.js.map +0 -1
- package/dist/openapi-generator-BEwyiaAr.js.map +0 -1
- package/dist/src-Cum9kox5.js.map +0 -1
- package/dist/src-_qQ3RNCK.js.map +0 -1
package/dist/index.es.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
import process from "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { n as ADMIN_PROPERTY_KEYS, t as ADMIN_COLLECTION_KEYS } from "./admin_block-
|
|
7
|
-
import {
|
|
8
|
-
import { n as createContractRoutes } from "./contract-routes-
|
|
9
|
-
import { $ as httpMethodToOperation, A as getEmailVerificationTemplate, B as optionalAuth, C as createDataRateLimiter, D as validatePasswordStrength, E as hashPassword, F as createAdapterAuthMiddleware, G as createApiKeyPreAuth, H as queryTokenAuth, I as createAuthMiddleware, J as isApiKeyToken, K as createFunctionApiKeyGuard, L as createRequireAuth, M as getPasswordResetTemplate, N as getUserInvitationTemplate, O as verifyPassword, P as getWelcomeEmailTemplate, Q as scopeDataDriver, R as extractUserFromToken, S as _coercedNumber, T as resolveAuthHooks, U as requireAdmin, V as publicObjectAuth, W as requireAuth, X as extractBearerToken, Y as validateApiKey, Z as safeCompare, _ as createBuiltinAuthAdapter, a as createSpotifyProvider, b as object, c as createGitLabProvider, d as createFacebookProvider, et as isOperationAllowed, f as createAppleProvider, g as createGoogleProvider, h as createLinkedinProvider, i as createApiKeyStore,
|
|
4
|
+
import { C as EntityReference, E as Vector, T as GeoPoint, _ as resolvePrimaryKeys, a as serializeFilter, b as resolveClientListLimit, c as paginateFind, d as createDataSourceRegistry, f as resolveDataSource, g as buildCompositeId, h as resolveCollectionRelations, i as deserializeLogicalCondition, l as resolveFindWindow, m as findRelation, n as buildSdkData, o as serializeLogicalCondition, p as getEffectiveSecurityRules, r as deserializeFilter, s as collectAllPages, t as buildRoutedRebaseData, u as CollectionRegistry, v as toSnakeCase, w as EntityRelation } from "./src-CvaxLgnM.js";
|
|
5
|
+
import { c as DEFAULT_DATA_SOURCE_KEY, i as getCollectionDataPath, n as RLS_ROLES_SQL, o as isPostgresCollectionConfig, p as toCanonicalOp, r as RLS_UID_SQL, t as RLS_JWT_SQL } from "./src-Ca6NhxKs.js";
|
|
6
|
+
import { n as ADMIN_PROPERTY_KEYS, t as ADMIN_COLLECTION_KEYS } from "./admin_block-CLeUMG8G.js";
|
|
7
|
+
import { n as isSQLAdmin, t as revokeInternalTableSql } from "./internal-tables-57VaN3o1.js";
|
|
8
|
+
import { n as createContractRoutes } from "./contract-routes-FSt0Nyfk.js";
|
|
9
|
+
import { $ as httpMethodToOperation, A as getEmailVerificationTemplate, B as optionalAuth, C as createDataRateLimiter, D as validatePasswordStrength, E as hashPassword, F as createAdapterAuthMiddleware, G as createApiKeyPreAuth, H as queryTokenAuth, I as createAuthMiddleware, J as isApiKeyToken, K as createFunctionApiKeyGuard, L as createRequireAuth, M as getPasswordResetTemplate, N as getUserInvitationTemplate, O as verifyPassword, P as getWelcomeEmailTemplate, Q as scopeDataDriver, R as extractUserFromToken, S as _coercedNumber, T as resolveAuthHooks, U as requireAdmin, V as publicObjectAuth, W as requireAuth, X as extractBearerToken, Y as validateApiKey, Z as safeCompare, _ as createBuiltinAuthAdapter, a as createSpotifyProvider, b as object, c as createGitLabProvider, d as createFacebookProvider, et as isOperationAllowed, f as createAppleProvider, g as createGoogleProvider, h as createLinkedinProvider, i as createApiKeyStore, j as getMagicLinkTemplate, k as generateSecurePassword, l as createDiscordProvider, m as createGitHubProvider, n as createCustomAuthAdapter, nt as isPublicStoragePath, o as createSlackProvider, p as createMicrosoftProvider, q as createStorageApiKeyGuard, r as createApiKeyRoutes, s as createBitbucketProvider, tt as PUBLIC_STORAGE_PREFIX, u as createTwitterProvider, v as ZodNumber, w as MemoryRateLimitStore, x as string, y as _enum, z as fileTokenAuth } from "./auth-CtxXQYV_.js";
|
|
10
10
|
import { t as logger } from "./logger-BYU66ENZ.js";
|
|
11
|
-
import { n as errorHandler,
|
|
12
|
-
import { i as generateDownloadToken, n as configureJwt } from "./jwt-
|
|
11
|
+
import { n as errorHandler, t as ApiError } from "./errors-BpudWAVU.js";
|
|
12
|
+
import { i as generateDownloadToken, n as configureJwt } from "./jwt-CzeviDcB.js";
|
|
13
13
|
import { t as logMiddleware } from "./logs-routes-BYA72C_C.js";
|
|
14
14
|
import { n as DOCUMENT_MIME_TYPES, r as IMAGE_MIME_TYPES, t as DEFAULT_MAX_FILE_SIZE } from "./types-DSnOC4mF.js";
|
|
15
15
|
import { t as S3StorageController } from "./S3StorageController-B2EDXXMH.js";
|
|
@@ -17,10 +17,10 @@ import { t as GCSStorageController } from "./GCSStorageController-BEmDYFKc.js";
|
|
|
17
17
|
import { n as loadFunctionsFromDirectory } from "./function-loader-B_1fYfUY.js";
|
|
18
18
|
import { t as createFunctionRoutes } from "./function-routes-C0cLIy3N.js";
|
|
19
19
|
import { n as loadCronJobsFromDirectory } from "./cron-loader-B1S2MCSl.js";
|
|
20
|
-
import { r as validateCronExpression, t as CronScheduler } from "./cron-scheduler-
|
|
21
|
-
import { t as createCronRoutes } from "./cron-routes-
|
|
22
|
-
import { t as createCronStore } from "./cron-store-
|
|
23
|
-
import { a as parseBackupTimestamp, i as parseBackupDestination, n as createBackupRoutes, o as readBackupBytes, r as listBackupObjects } from "./backup-
|
|
20
|
+
import { r as validateCronExpression, t as CronScheduler } from "./cron-scheduler-CsQtYLMq.js";
|
|
21
|
+
import { t as createCronRoutes } from "./cron-routes-D5a9v1HY.js";
|
|
22
|
+
import { t as createCronStore } from "./cron-store-CHH7zrif.js";
|
|
23
|
+
import { a as parseBackupTimestamp, i as parseBackupDestination, n as createBackupRoutes, o as readBackupBytes, r as listBackupObjects } from "./backup-BJiY_mM-.js";
|
|
24
24
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
25
25
|
import * as fs$2 from "fs";
|
|
26
26
|
import fs, { existsSync } from "fs";
|
|
@@ -75,7 +75,7 @@ import { promisify as promisify$1 } from "node:util";
|
|
|
75
75
|
var RebaseApiError = class extends Error {
|
|
76
76
|
/** HTTP status code, or `undefined` for non-HTTP errors. */
|
|
77
77
|
status;
|
|
78
|
-
/** Stable machine-readable error code, when the server supplied one. */
|
|
78
|
+
/** Stable machine-readable error code, when the server supplied one. See {@link RebaseErrorCode}. */
|
|
79
79
|
code;
|
|
80
80
|
/** Structured error payload from the server, when present. */
|
|
81
81
|
details;
|
|
@@ -869,7 +869,12 @@ function parseLogicalGroup(type, raw) {
|
|
|
869
869
|
if (inner.startsWith("(") && inner.endsWith(")")) inner = inner.slice(1, -1);
|
|
870
870
|
inner = inner.trim();
|
|
871
871
|
if (!inner) return void 0;
|
|
872
|
-
|
|
872
|
+
let parsed;
|
|
873
|
+
try {
|
|
874
|
+
parsed = deserializeLogicalCondition(`${type}(${inner})`);
|
|
875
|
+
} catch (e) {
|
|
876
|
+
throw ApiError.badRequest(`Invalid \`${type}\` parameter: ${e instanceof Error ? e.message : String(e)}`, "INVALID_LOGICAL_GROUP");
|
|
877
|
+
}
|
|
873
878
|
return "type" in parsed ? parsed : void 0;
|
|
874
879
|
}
|
|
875
880
|
/**
|
|
@@ -899,6 +904,73 @@ function parseWhereParam(raw) {
|
|
|
899
904
|
const filter = deserializeFilter(parsed);
|
|
900
905
|
return Object.keys(filter).length > 0 ? filter : void 0;
|
|
901
906
|
}
|
|
907
|
+
function invalidOrderBy(detail) {
|
|
908
|
+
throw ApiError.badRequest(`Invalid \`orderBy\` parameter: ${detail}. Expected \`field\`, \`field:desc\`, or a JSON array like [{"field":"created_at","direction":"desc"}]`, "INVALID_ORDER_BY");
|
|
909
|
+
}
|
|
910
|
+
/** `asc`/`desc`, in any case. Anything else is a request to sort in a way that does not exist. */
|
|
911
|
+
function toDirection(raw, context) {
|
|
912
|
+
if (raw === void 0 || raw === null) return "asc";
|
|
913
|
+
if (typeof raw !== "string") invalidOrderBy(`${context} has a non-string \`direction\``);
|
|
914
|
+
const lowered = raw.toLowerCase();
|
|
915
|
+
if (lowered !== "asc" && lowered !== "desc") invalidOrderBy(`${context} has direction '${raw}'`);
|
|
916
|
+
return lowered;
|
|
917
|
+
}
|
|
918
|
+
/** One entry: the canonical `{field, direction}`, or the `field:direction` shorthand as a string. */
|
|
919
|
+
function toOrderByEntry(raw, index) {
|
|
920
|
+
const context = `entry ${index}`;
|
|
921
|
+
if (typeof raw === "string") {
|
|
922
|
+
const tuple = deserializeOrderBy(raw);
|
|
923
|
+
if (!tuple || !tuple[0]) invalidOrderBy(`${context} is an empty field name`);
|
|
924
|
+
return {
|
|
925
|
+
field: tuple[0],
|
|
926
|
+
direction: toDirection(tuple[1], context)
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) invalidOrderBy(`${context} is not a field name or a {field, direction} object`);
|
|
930
|
+
const entry = raw;
|
|
931
|
+
if (typeof entry.field !== "string" || entry.field.trim() === "") invalidOrderBy(`${context} has no \`field\``);
|
|
932
|
+
return {
|
|
933
|
+
field: entry.field,
|
|
934
|
+
direction: toDirection(entry.direction, context)
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Parse the `orderBy` query parameter.
|
|
939
|
+
*
|
|
940
|
+
* The field *name* has been validated against the schema for a while — an
|
|
941
|
+
* `?orderBy=titel` is a 400 rather than 200 with unsorted rows, on the grounds
|
|
942
|
+
* that silently dropping the sort leaves the caller believing in an order that
|
|
943
|
+
* is not there. The parameter's *shape* was never checked the same way, and it
|
|
944
|
+
* failed in exactly the same silent manner one layer earlier: whatever
|
|
945
|
+
* `JSON.parse` returned was assigned to an option declared as an array of
|
|
946
|
+
* `{field, direction}`, and the REST layer reads only `orderBy[0].field`. So
|
|
947
|
+
* `?orderBy={"field":"name"}` — an object rather than an array, and the most
|
|
948
|
+
* natural thing for a client to try — read `undefined`, dropped the ORDER BY,
|
|
949
|
+
* and answered 200. So did a number, a boolean, `null`, and `["name"]`.
|
|
950
|
+
*
|
|
951
|
+
* This refuses those, the way `parseWhereParam` above already refuses a
|
|
952
|
+
* malformed filter and for the same reason. What it keeps working is every
|
|
953
|
+
* shape that worked before: the `field` and `field:desc` shorthands, and the
|
|
954
|
+
* canonical JSON array.
|
|
955
|
+
*/
|
|
956
|
+
function parseOrderByParam(raw) {
|
|
957
|
+
if (Array.isArray(raw)) return raw.length === 0 ? void 0 : raw.map(toOrderByEntry);
|
|
958
|
+
const str = String(raw).trim();
|
|
959
|
+
if (!str) return void 0;
|
|
960
|
+
let parsed;
|
|
961
|
+
try {
|
|
962
|
+
parsed = JSON.parse(str);
|
|
963
|
+
} catch {
|
|
964
|
+
return [toOrderByEntry(str, 0)];
|
|
965
|
+
}
|
|
966
|
+
if (Array.isArray(parsed)) {
|
|
967
|
+
if (parsed.length === 0) return void 0;
|
|
968
|
+
return parsed.map(toOrderByEntry);
|
|
969
|
+
}
|
|
970
|
+
if (typeof parsed === "string") return [toOrderByEntry(parsed, 0)];
|
|
971
|
+
if (typeof parsed === "object" && parsed !== null) invalidOrderBy("a single object was given where a JSON array was expected");
|
|
972
|
+
invalidOrderBy(`${typeof parsed} is not a field name or a list of them`);
|
|
973
|
+
}
|
|
902
974
|
/**
|
|
903
975
|
* Parse query parameters into QueryOptions
|
|
904
976
|
*/
|
|
@@ -953,17 +1025,7 @@ function parseQueryOptions(query, limits = {}) {
|
|
|
953
1025
|
};
|
|
954
1026
|
if (Object.keys(where).length > 0) options.where = where;
|
|
955
1027
|
const orderByVal = getLastValue(query.orderBy);
|
|
956
|
-
if (orderByVal)
|
|
957
|
-
options.orderBy = typeof orderByVal === "string" ? JSON.parse(orderByVal) : orderByVal;
|
|
958
|
-
} catch {
|
|
959
|
-
if (typeof orderByVal === "string") {
|
|
960
|
-
const parsed = deserializeOrderBy(orderByVal);
|
|
961
|
-
if (parsed) options.orderBy = [{
|
|
962
|
-
field: parsed[0],
|
|
963
|
-
direction: parsed[1]
|
|
964
|
-
}];
|
|
965
|
-
}
|
|
966
|
-
}
|
|
1028
|
+
if (orderByVal) options.orderBy = parseOrderByParam(orderByVal);
|
|
967
1029
|
const includeVal = getLastValue(query.include);
|
|
968
1030
|
if (includeVal) {
|
|
969
1031
|
const includeStr = String(includeVal).trim();
|
|
@@ -1055,11 +1117,18 @@ function assertKnownWriteFields(values, collection, options) {
|
|
|
1055
1117
|
* This shapes the *response*, which is what the parameter says it does; it is
|
|
1056
1118
|
* not a column pushdown, so it saves bandwidth rather than database work.
|
|
1057
1119
|
*
|
|
1058
|
-
*
|
|
1059
|
-
* the admin table, realtime reconciliation, the offline
|
|
1060
|
-
* arrives without one is not a smaller row, it is an
|
|
1061
|
-
* `fields=title` and being unable to open the record
|
|
1062
|
-
* answer than one extra key.
|
|
1120
|
+
* The collection's key always survives. Rows are addressed by it everywhere
|
|
1121
|
+
* above this layer — the admin table, realtime reconciliation, the offline
|
|
1122
|
+
* cache — and a row that arrives without one is not a smaller row, it is an
|
|
1123
|
+
* unusable one. Asking for `fields=title` and being unable to open the record
|
|
1124
|
+
* you clicked is a worse answer than one extra key.
|
|
1125
|
+
*
|
|
1126
|
+
* That is a statement about the key, not about the name `id`: a collection
|
|
1127
|
+
* keyed on `slug`, or on `user_id + role_id`, was projected down to the fields
|
|
1128
|
+
* asked for and nothing else, which is the very outcome the paragraph above
|
|
1129
|
+
* describes. `assertKnownWriteFields` one function up already has a dedicated
|
|
1130
|
+
* error for collections with no `id` column, so this layer had no excuse for
|
|
1131
|
+
* assuming one.
|
|
1063
1132
|
*/
|
|
1064
1133
|
function projectResponseFields(rows, fields, collection, options) {
|
|
1065
1134
|
if (!fields || fields.length === 0) return rows;
|
|
@@ -1077,7 +1146,8 @@ function projectResponseFields(rows, fields, collection, options) {
|
|
|
1077
1146
|
collection: collection.slug
|
|
1078
1147
|
});
|
|
1079
1148
|
}
|
|
1080
|
-
const
|
|
1149
|
+
const primaryKeys = resolvePrimaryKeys(collection).map((key) => key.fieldName);
|
|
1150
|
+
const keep = /* @__PURE__ */ new Set([...fields, ...primaryKeys.length > 0 ? primaryKeys : ["id"]]);
|
|
1081
1151
|
return rows.map((row) => {
|
|
1082
1152
|
const projected = {};
|
|
1083
1153
|
for (const key of Object.keys(row)) if (keep.has(key)) projected[key] = row[key];
|
|
@@ -1152,6 +1222,7 @@ function createIdempotencyStore(driver) {
|
|
|
1152
1222
|
)
|
|
1153
1223
|
`);
|
|
1154
1224
|
await exec(`CREATE INDEX IF NOT EXISTS idx_idempotency_created ON ${TABLE}(created_at)`);
|
|
1225
|
+
await exec(revokeInternalTableSql("rebase", "idempotency_keys"));
|
|
1155
1226
|
return true;
|
|
1156
1227
|
} catch (error) {
|
|
1157
1228
|
logger.warn("Idempotency keys unavailable — a replayed offline write may insert a duplicate row.", { detail: error instanceof Error ? error.message : String(error) });
|
|
@@ -1161,26 +1232,43 @@ function createIdempotencyStore(driver) {
|
|
|
1161
1232
|
return ready;
|
|
1162
1233
|
};
|
|
1163
1234
|
return {
|
|
1164
|
-
async
|
|
1165
|
-
if (!key || !await ensure()) return
|
|
1235
|
+
async claim(key, uid) {
|
|
1236
|
+
if (!key || !await ensure()) return { status: "claimed" };
|
|
1166
1237
|
try {
|
|
1167
|
-
|
|
1168
|
-
|
|
1238
|
+
if ((await exec(`INSERT INTO ${TABLE} (key, uid, response, created_at)
|
|
1239
|
+
VALUES ($1, $2, NULL, NOW())
|
|
1240
|
+
ON CONFLICT (uid, key) DO UPDATE
|
|
1241
|
+
SET response = NULL, created_at = NOW()
|
|
1242
|
+
WHERE ${TABLE}.created_at < NOW() - INTERVAL '${TTL_HOURS} hours'
|
|
1243
|
+
RETURNING 1 AS claimed`, [key, principal(uid)])).length > 0) return { status: "claimed" };
|
|
1244
|
+
const row = (await exec(`SELECT response, response IS NULL AS pending FROM ${TABLE}
|
|
1245
|
+
WHERE uid = $1 AND key = $2`, [principal(uid), key]))[0];
|
|
1246
|
+
if (!row) return { status: "claimed" };
|
|
1247
|
+
if (row.pending) return { status: "in-flight" };
|
|
1248
|
+
return {
|
|
1249
|
+
status: "replay",
|
|
1250
|
+
response: row.response
|
|
1251
|
+
};
|
|
1169
1252
|
} catch {
|
|
1170
|
-
return;
|
|
1253
|
+
return { status: "claimed" };
|
|
1171
1254
|
}
|
|
1172
1255
|
},
|
|
1173
|
-
async
|
|
1256
|
+
async complete(key, uid, response) {
|
|
1174
1257
|
if (!key || !await ensure()) return;
|
|
1175
1258
|
try {
|
|
1176
|
-
await exec(`
|
|
1177
|
-
ON CONFLICT (uid, key) DO NOTHING`, [
|
|
1178
|
-
key,
|
|
1259
|
+
await exec(`UPDATE ${TABLE} SET response = $3::jsonb WHERE uid = $1 AND key = $2`, [
|
|
1179
1260
|
principal(uid),
|
|
1261
|
+
key,
|
|
1180
1262
|
JSON.stringify(response ?? null)
|
|
1181
1263
|
]);
|
|
1182
1264
|
if (Math.random() < .01) await exec(`DELETE FROM ${TABLE} WHERE created_at < NOW() - INTERVAL '${TTL_HOURS} hours'`);
|
|
1183
1265
|
} catch {}
|
|
1266
|
+
},
|
|
1267
|
+
async release(key, uid) {
|
|
1268
|
+
if (!key || !await ensure()) return;
|
|
1269
|
+
try {
|
|
1270
|
+
await exec(`DELETE FROM ${TABLE} WHERE uid = $1 AND key = $2 AND response IS NULL`, [principal(uid), key]);
|
|
1271
|
+
} catch {}
|
|
1184
1272
|
}
|
|
1185
1273
|
};
|
|
1186
1274
|
}
|
|
@@ -1367,121 +1455,225 @@ var RestApiGenerator = class {
|
|
|
1367
1455
|
if (!entity) throw ApiError.notFound("Entity not found");
|
|
1368
1456
|
return c.json(projectResponseFields([entity], queryOptions.fields, resolvedCollection, { include: queryOptions.include })[0]);
|
|
1369
1457
|
});
|
|
1458
|
+
/**
|
|
1459
|
+
* Run a bulk handler under an idempotency key.
|
|
1460
|
+
*
|
|
1461
|
+
* All three bulk routes need the identical claim-before-write dance —
|
|
1462
|
+
* claim, replay on a repeat, 409 while one is in flight, release on
|
|
1463
|
+
* failure, complete on success — and getting one of those steps wrong
|
|
1464
|
+
* is exactly the bug the key exists to prevent. Written once so the
|
|
1465
|
+
* three cannot drift into three different notions of "already done".
|
|
1466
|
+
*/
|
|
1467
|
+
const withIdempotency = async (c, run, respond) => {
|
|
1468
|
+
const idempotencyKey = c.req.header(IDEMPOTENCY_HEADER);
|
|
1469
|
+
const uid = c.get("user")?.uid;
|
|
1470
|
+
const store = this.idempotency();
|
|
1471
|
+
const claimed = idempotencyKey && store ? await store.claim(idempotencyKey, uid) : void 0;
|
|
1472
|
+
if (claimed?.status === "replay") return respond(claimed.response);
|
|
1473
|
+
if (claimed?.status === "in-flight") throw ApiError.conflict(`A request with Idempotency-Key '${idempotencyKey}' is already in progress. Retry once it has answered; its result will be replayed.`, "IDEMPOTENCY_KEY_IN_PROGRESS");
|
|
1474
|
+
let response;
|
|
1475
|
+
try {
|
|
1476
|
+
response = await run();
|
|
1477
|
+
} catch (error) {
|
|
1478
|
+
if (claimed?.status === "claimed" && idempotencyKey && store) await store.release(idempotencyKey, uid);
|
|
1479
|
+
throw error;
|
|
1480
|
+
}
|
|
1481
|
+
if (claimed?.status === "claimed" && idempotencyKey && store) await store.complete(idempotencyKey, uid, response);
|
|
1482
|
+
return respond(response);
|
|
1483
|
+
};
|
|
1484
|
+
/**
|
|
1485
|
+
* Validate the envelope every bulk route shares: an array, non-empty,
|
|
1486
|
+
* of objects, within the row cap.
|
|
1487
|
+
*
|
|
1488
|
+
* The cap is the load-bearing one. A batch is a single transaction that
|
|
1489
|
+
* holds its locks for its whole duration, so an unbounded one is a
|
|
1490
|
+
* self-inflicted outage — and the message names both the limit and the
|
|
1491
|
+
* actual count so the caller can chunk to it rather than guess.
|
|
1492
|
+
*/
|
|
1493
|
+
const assertBulkShape = (items, field, requireObjects) => {
|
|
1494
|
+
if (!Array.isArray(items)) throw ApiError.badRequest(`Expected a JSON body of { ${field}: [...] }.`, "INVALID_BULK_BODY");
|
|
1495
|
+
if (requireObjects && items.some((it) => typeof it !== "object" || it === null || Array.isArray(it))) throw ApiError.badRequest(`Every entry in \`${field}\` must be an object.`, "INVALID_BULK_BODY");
|
|
1496
|
+
if (items.length > this.maxBulkRows) throw ApiError.badRequest(`Too many rows: ${items.length} exceeds the ${this.maxBulkRows}-row limit for a single bulk write. Send it in chunks of ${this.maxBulkRows} or fewer.`, "BULK_TOO_LARGE");
|
|
1497
|
+
};
|
|
1370
1498
|
this.router.post(`${basePath}/bulk`, async (c) => {
|
|
1371
1499
|
this.enforceApiKeyPermission(c, collection.slug);
|
|
1372
1500
|
const driver = this.getScopedDriver(c);
|
|
1373
1501
|
const path = collection.slug;
|
|
1374
1502
|
const body = await parseJsonBody(c);
|
|
1375
|
-
|
|
1376
|
-
|
|
1503
|
+
assertBulkShape(body?.rows, "rows", true);
|
|
1504
|
+
const rows = body.rows;
|
|
1505
|
+
if (rows.length === 0) return c.json({
|
|
1377
1506
|
data: [],
|
|
1378
1507
|
meta: { written: 0 }
|
|
1379
1508
|
});
|
|
1380
|
-
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");
|
|
1381
1509
|
if (body.upsert !== void 0 && typeof body.upsert !== "boolean") throw ApiError.badRequest("`upsert` must be a boolean.", "INVALID_BULK_BODY");
|
|
1382
|
-
const maxRows = this.maxBulkRows;
|
|
1383
|
-
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");
|
|
1384
1510
|
if (!driver.saveMany) throw ApiError.badRequest("This collection's data source does not support bulk writes.", "BULK_UNSUPPORTED");
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1511
|
+
rows.forEach((row, rowIndex) => assertKnownWriteFields(row, resolvedCollection, { rowIndex }));
|
|
1512
|
+
return withIdempotency(c, async () => {
|
|
1513
|
+
const written = await driver.saveMany({
|
|
1514
|
+
path,
|
|
1515
|
+
rows,
|
|
1516
|
+
collection: resolvedCollection,
|
|
1517
|
+
upsert: body.upsert === true
|
|
1518
|
+
});
|
|
1519
|
+
return {
|
|
1520
|
+
data: written.map((row) => this.formatResponse(row)),
|
|
1521
|
+
meta: { written: written.length }
|
|
1522
|
+
};
|
|
1523
|
+
}, (result) => c.json(result));
|
|
1524
|
+
});
|
|
1525
|
+
this.router.patch(`${basePath}/bulk`, async (c) => {
|
|
1526
|
+
this.enforceApiKeyPermission(c, collection.slug);
|
|
1527
|
+
const driver = this.getScopedDriver(c);
|
|
1528
|
+
const path = collection.slug;
|
|
1529
|
+
const body = await parseJsonBody(c);
|
|
1530
|
+
assertBulkShape(body?.updates, "updates", true);
|
|
1531
|
+
const updates = body.updates;
|
|
1532
|
+
if (updates.length === 0) return c.json({
|
|
1533
|
+
data: [],
|
|
1534
|
+
meta: { written: 0 }
|
|
1391
1535
|
});
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1536
|
+
if (!driver.updateMany) throw ApiError.badRequest("This collection's data source does not support bulk updates.", "BULK_UNSUPPORTED");
|
|
1537
|
+
updates.forEach((entry, rowIndex) => {
|
|
1538
|
+
if (entry.id === void 0 || entry.id === null || entry.id === "") throw ApiError.badRequest(`Entry ${rowIndex} of \`updates\` is missing \`id\`. Each entry must be { id, data }.`, "INVALID_BULK_BODY");
|
|
1539
|
+
if (typeof entry.data !== "object" || entry.data === null || Array.isArray(entry.data)) throw ApiError.badRequest(`Entry ${rowIndex} of \`updates\` is missing \`data\`, or it is not an object.`, "INVALID_BULK_BODY");
|
|
1540
|
+
assertKnownWriteFields(entry.data, resolvedCollection, { rowIndex });
|
|
1395
1541
|
});
|
|
1542
|
+
return withIdempotency(c, async () => {
|
|
1543
|
+
const written = await driver.updateMany({
|
|
1544
|
+
path,
|
|
1545
|
+
updates: updates.map((entry) => ({
|
|
1546
|
+
id: entry.id,
|
|
1547
|
+
values: entry.data
|
|
1548
|
+
})),
|
|
1549
|
+
collection: resolvedCollection
|
|
1550
|
+
});
|
|
1551
|
+
return {
|
|
1552
|
+
data: written.map((row) => this.formatResponse(row)),
|
|
1553
|
+
meta: { written: written.length }
|
|
1554
|
+
};
|
|
1555
|
+
}, (result) => c.json(result));
|
|
1556
|
+
});
|
|
1557
|
+
this.router.post(`${basePath}/bulk/delete`, async (c) => {
|
|
1558
|
+
this.enforceApiKeyPermission(c, collection.slug);
|
|
1559
|
+
const driver = this.getScopedDriver(c);
|
|
1560
|
+
const path = collection.slug;
|
|
1561
|
+
const body = await parseJsonBody(c);
|
|
1562
|
+
assertBulkShape(body?.ids, "ids", false);
|
|
1563
|
+
const ids = body.ids;
|
|
1564
|
+
if (ids.length === 0) return c.json({ meta: { deleted: 0 } });
|
|
1565
|
+
if (ids.some((id) => typeof id !== "string" && typeof id !== "number")) throw ApiError.badRequest("Every entry in `ids` must be a string or a number.", "INVALID_BULK_BODY");
|
|
1566
|
+
if (!driver.deleteMany) throw ApiError.badRequest("This collection's data source does not support bulk deletes.", "BULK_UNSUPPORTED");
|
|
1567
|
+
return withIdempotency(c, async () => {
|
|
1568
|
+
await driver.deleteMany({
|
|
1569
|
+
path,
|
|
1570
|
+
ids,
|
|
1571
|
+
collection: resolvedCollection
|
|
1572
|
+
});
|
|
1573
|
+
return { meta: { deleted: ids.length } };
|
|
1574
|
+
}, (result) => c.json(result));
|
|
1396
1575
|
});
|
|
1397
1576
|
this.router.post(basePath, async (c) => {
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
const prepared = await this.authAdapter.prepareUserCreation(body, collectionAuthConfig);
|
|
1413
|
-
const entity = await driver.save({
|
|
1414
|
-
path,
|
|
1415
|
-
values: prepared.values,
|
|
1416
|
-
collection: resolvedCollection,
|
|
1417
|
-
status: "new"
|
|
1418
|
-
});
|
|
1419
|
-
const result = prepared.hookHandledEmail ? {
|
|
1420
|
-
temporaryPassword: prepared.clearPassword,
|
|
1421
|
-
invitationSent: prepared.invitationSent
|
|
1422
|
-
} : this.authAdapter.finalizeUserCreation ? await this.authAdapter.finalizeUserCreation({
|
|
1423
|
-
id: entity.id,
|
|
1424
|
-
values: entity
|
|
1425
|
-
}, prepared.clearPassword) : { invitationSent: false };
|
|
1426
|
-
const response = this.formatResponse(entity);
|
|
1427
|
-
return c.json({
|
|
1428
|
-
...response,
|
|
1429
|
-
invitationSent: result.invitationSent,
|
|
1430
|
-
...result.temporaryPassword ? { temporaryPassword: result.temporaryPassword } : {},
|
|
1431
|
-
..."emailDeliveryFailed" in result && result.emailDeliveryFailed ? { emailDeliveryFailed: true } : {}
|
|
1432
|
-
}, 201);
|
|
1433
|
-
}
|
|
1434
|
-
const idempotencyKey = c.req.header(IDEMPOTENCY_HEADER);
|
|
1435
|
-
const uid = c.get("user")?.uid;
|
|
1436
|
-
const store = this.idempotency();
|
|
1437
|
-
if (idempotencyKey && store) {
|
|
1438
|
-
const already = await store.recall(idempotencyKey, uid);
|
|
1439
|
-
if (already !== void 0) return c.json(already, 201);
|
|
1440
|
-
}
|
|
1577
|
+
this.enforceApiKeyPermission(c, collection.slug);
|
|
1578
|
+
const driver = this.getScopedDriver(c);
|
|
1579
|
+
const path = collection.slug;
|
|
1580
|
+
const body = await parseJsonBody(c);
|
|
1581
|
+
const isAuth = collection.auth;
|
|
1582
|
+
const isAuthCollection = isAuth === true || isAuth && typeof isAuth === "object" && isAuth.enabled === true;
|
|
1583
|
+
const collectionAuthConfig = typeof isAuth === "object" ? isAuth : void 0;
|
|
1584
|
+
if (!isAuthCollection) assertKnownWriteFields(body, resolvedCollection);
|
|
1585
|
+
else {
|
|
1586
|
+
const contract = this.authAdapter?.describeUserCreationContract?.(collectionAuthConfig);
|
|
1587
|
+
if (contract?.validate) assertKnownWriteFields(body, resolvedCollection, { extraKnownFields: contract.extraFields });
|
|
1588
|
+
}
|
|
1589
|
+
if (isAuthCollection && this.authAdapter?.prepareUserCreation) {
|
|
1590
|
+
const prepared = await this.authAdapter.prepareUserCreation(body, collectionAuthConfig);
|
|
1441
1591
|
const entity = await driver.save({
|
|
1442
1592
|
path,
|
|
1443
|
-
values:
|
|
1593
|
+
values: prepared.values,
|
|
1444
1594
|
collection: resolvedCollection,
|
|
1445
1595
|
status: "new"
|
|
1446
1596
|
});
|
|
1597
|
+
const result = prepared.hookHandledEmail ? {
|
|
1598
|
+
temporaryPassword: prepared.clearPassword,
|
|
1599
|
+
invitationSent: prepared.invitationSent
|
|
1600
|
+
} : this.authAdapter.finalizeUserCreation ? await this.authAdapter.finalizeUserCreation({
|
|
1601
|
+
id: entity.id,
|
|
1602
|
+
values: entity
|
|
1603
|
+
}, prepared.clearPassword) : { invitationSent: false };
|
|
1447
1604
|
const response = this.formatResponse(entity);
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
}
|
|
1454
|
-
throw error;
|
|
1605
|
+
return c.json({
|
|
1606
|
+
...response,
|
|
1607
|
+
invitationSent: result.invitationSent,
|
|
1608
|
+
...result.temporaryPassword ? { temporaryPassword: result.temporaryPassword } : {},
|
|
1609
|
+
..."emailDeliveryFailed" in result && result.emailDeliveryFailed ? { emailDeliveryFailed: true } : {}
|
|
1610
|
+
}, 201);
|
|
1455
1611
|
}
|
|
1456
|
-
|
|
1457
|
-
|
|
1612
|
+
const idempotencyKey = c.req.header(IDEMPOTENCY_HEADER);
|
|
1613
|
+
const uid = c.get("user")?.uid;
|
|
1614
|
+
const store = this.idempotency();
|
|
1615
|
+
const claimed = idempotencyKey && store ? await store.claim(idempotencyKey, uid) : void 0;
|
|
1616
|
+
if (claimed?.status === "replay") return c.json(claimed.response, 201);
|
|
1617
|
+
if (claimed?.status === "in-flight") throw ApiError.conflict(`A request with Idempotency-Key '${idempotencyKey}' is already in progress. Retry once it has answered; its result will be replayed.`, "IDEMPOTENCY_KEY_IN_PROGRESS");
|
|
1618
|
+
let response;
|
|
1458
1619
|
try {
|
|
1459
|
-
this.enforceApiKeyPermission(c, collection.slug);
|
|
1460
|
-
const id = c.req.param("id");
|
|
1461
|
-
const driver = this.getScopedDriver(c);
|
|
1462
|
-
if (!await driver.fetchOne({
|
|
1463
|
-
path: getCollectionDataPath(collection),
|
|
1464
|
-
id: String(id),
|
|
1465
|
-
collection: resolvedCollection
|
|
1466
|
-
})) throw ApiError.notFound("Entity not found");
|
|
1467
|
-
const body = await parseJsonBody(c);
|
|
1468
|
-
assertKnownWriteFields(body, resolvedCollection);
|
|
1469
1620
|
const entity = await driver.save({
|
|
1470
|
-
path
|
|
1471
|
-
id: String(id),
|
|
1621
|
+
path,
|
|
1472
1622
|
values: body,
|
|
1473
1623
|
collection: resolvedCollection,
|
|
1474
|
-
status: "
|
|
1624
|
+
status: "new"
|
|
1475
1625
|
});
|
|
1476
|
-
|
|
1477
|
-
return c.json(response);
|
|
1626
|
+
response = this.formatResponse(entity);
|
|
1478
1627
|
} catch (error) {
|
|
1479
|
-
if (
|
|
1480
|
-
if (!(error instanceof TypeError || error instanceof RangeError || error instanceof SyntaxError || error instanceof ReferenceError)) error.code = "BAD_REQUEST";
|
|
1481
|
-
}
|
|
1628
|
+
if (claimed?.status === "claimed" && idempotencyKey && store) await store.release(idempotencyKey, uid);
|
|
1482
1629
|
throw error;
|
|
1483
1630
|
}
|
|
1631
|
+
if (claimed?.status === "claimed" && idempotencyKey && store) await store.complete(idempotencyKey, uid, response);
|
|
1632
|
+
return c.json(response, 201);
|
|
1484
1633
|
});
|
|
1634
|
+
const updateEntity = async (c) => {
|
|
1635
|
+
this.enforceApiKeyPermission(c, collection.slug);
|
|
1636
|
+
const id = c.req.param("id");
|
|
1637
|
+
const driver = this.getScopedDriver(c);
|
|
1638
|
+
if (!await driver.fetchOne({
|
|
1639
|
+
path: getCollectionDataPath(collection),
|
|
1640
|
+
id: String(id),
|
|
1641
|
+
collection: resolvedCollection
|
|
1642
|
+
})) throw ApiError.notFound("Entity not found");
|
|
1643
|
+
const body = await parseJsonBody(c);
|
|
1644
|
+
assertKnownWriteFields(body, resolvedCollection);
|
|
1645
|
+
const entity = await driver.save({
|
|
1646
|
+
path: getCollectionDataPath(collection),
|
|
1647
|
+
id: String(id),
|
|
1648
|
+
values: body,
|
|
1649
|
+
collection: resolvedCollection,
|
|
1650
|
+
status: "existing"
|
|
1651
|
+
});
|
|
1652
|
+
const response = this.formatResponse(entity);
|
|
1653
|
+
return c.json(response);
|
|
1654
|
+
};
|
|
1655
|
+
/**
|
|
1656
|
+
* The verb that matches what this actually does.
|
|
1657
|
+
*
|
|
1658
|
+
* The handler merges: it writes the columns in the body and leaves the
|
|
1659
|
+
* rest alone, which is what the SDK's `update(id, data: Partial<M>)`
|
|
1660
|
+
* sends. PUT was the only route, and PUT means replace — so the
|
|
1661
|
+
* generated OpenAPI spec described a full replacement while the server
|
|
1662
|
+
* performed a merge, and it reused the *create* input schema for it,
|
|
1663
|
+
* `required` fields and all. A client generated from that spec was
|
|
1664
|
+
* given a contract the server does not implement, and a spec-validating
|
|
1665
|
+
* gateway in front of this API would reject partial updates the server
|
|
1666
|
+
* would have accepted.
|
|
1667
|
+
*
|
|
1668
|
+
* PUT stays mounted on the same handler rather than being changed or
|
|
1669
|
+
* removed: every published SDK sends it, and altering its semantics to
|
|
1670
|
+
* be a true replace would silently start nulling columns that callers
|
|
1671
|
+
* had simply omitted for years — a data-loss change disguised as a
|
|
1672
|
+
* standards fix. So PATCH is the honest name, PUT is the compatible one,
|
|
1673
|
+
* and both do the documented thing.
|
|
1674
|
+
*/
|
|
1675
|
+
this.router.patch(`${basePath}/:id`, updateEntity);
|
|
1676
|
+
this.router.put(`${basePath}/:id`, updateEntity);
|
|
1485
1677
|
this.router.delete(`${basePath}/:id`, async (c) => {
|
|
1486
1678
|
this.enforceApiKeyPermission(c, collection.slug);
|
|
1487
1679
|
const id = c.req.param("id");
|
|
@@ -1552,6 +1744,7 @@ var RestApiGenerator = class {
|
|
|
1552
1744
|
const total = driver.count ? await driver.count({
|
|
1553
1745
|
path: parsed.collectionPath,
|
|
1554
1746
|
filter: queryOptions.where,
|
|
1747
|
+
logical: queryOptions.logical,
|
|
1555
1748
|
searchString
|
|
1556
1749
|
}) : 0;
|
|
1557
1750
|
return c.json({ count: total });
|
|
@@ -1563,7 +1756,9 @@ var RestApiGenerator = class {
|
|
|
1563
1756
|
id: parsed.id
|
|
1564
1757
|
});
|
|
1565
1758
|
if (!entity) throw ApiError.notFound("Entity not found");
|
|
1566
|
-
|
|
1759
|
+
const nestedCollection = this.resolveNestedWriteCollection(parsed.collectionPath);
|
|
1760
|
+
if (!nestedCollection) return c.json(entity);
|
|
1761
|
+
return c.json(projectResponseFields([entity], queryOptions.fields, nestedCollection, { include: queryOptions.include })[0]);
|
|
1567
1762
|
} else {
|
|
1568
1763
|
const queryDict = c.req.queries();
|
|
1569
1764
|
const queryOptions = this.parseQuery(queryDict);
|
|
@@ -1588,8 +1783,9 @@ var RestApiGenerator = class {
|
|
|
1588
1783
|
logical: queryOptions.logical,
|
|
1589
1784
|
searchString
|
|
1590
1785
|
}) : entities.length;
|
|
1786
|
+
const listCollection = this.resolveNestedWriteCollection(parsed.collectionPath);
|
|
1591
1787
|
return c.json({
|
|
1592
|
-
data: entities,
|
|
1788
|
+
data: listCollection ? projectResponseFields(entities, queryOptions.fields, listCollection, { include: queryOptions.include }) : entities,
|
|
1593
1789
|
meta: {
|
|
1594
1790
|
total,
|
|
1595
1791
|
limit: queryOptions.limit,
|
|
@@ -1618,7 +1814,7 @@ var RestApiGenerator = class {
|
|
|
1618
1814
|
const response = this.formatResponse(entity);
|
|
1619
1815
|
return c.json(response, 201);
|
|
1620
1816
|
});
|
|
1621
|
-
|
|
1817
|
+
const updateNested = async (c, next) => {
|
|
1622
1818
|
const rest = c.req.param("rest");
|
|
1623
1819
|
if (!rest || rest === "undefined") return next();
|
|
1624
1820
|
const rawPath = `${c.req.param("parent")}/${c.req.param("parentId")}/${rest}`;
|
|
@@ -1637,7 +1833,9 @@ var RestApiGenerator = class {
|
|
|
1637
1833
|
});
|
|
1638
1834
|
const response = this.formatResponse(entity);
|
|
1639
1835
|
return c.json(response);
|
|
1640
|
-
}
|
|
1836
|
+
};
|
|
1837
|
+
this.router.patch("/:parent/:parentId/:rest{.+}", updateNested);
|
|
1838
|
+
this.router.put("/:parent/:parentId/:rest{.+}", updateNested);
|
|
1641
1839
|
this.router.delete("/:parent/:parentId/:rest{.+}", async (c, next) => {
|
|
1642
1840
|
const rest = c.req.param("rest");
|
|
1643
1841
|
if (!rest || rest === "undefined") return next();
|
|
@@ -3081,7 +3279,7 @@ function assertStorageAccessControlConfigured(state, isProduction) {
|
|
|
3081
3279
|
//#region src/init/docs.ts
|
|
3082
3280
|
async function mountOpenApiDocs(app, basePath, enableSwagger, activeCollections, requireAuth) {
|
|
3083
3281
|
if (enableSwagger === false || activeCollections.length === 0) return;
|
|
3084
|
-
const { generateOpenApiSpec } = await import("./openapi-generator-
|
|
3282
|
+
const { generateOpenApiSpec } = await import("./openapi-generator-CSlEZh7-.js");
|
|
3085
3283
|
app.get(`${basePath}/docs`, (c) => {
|
|
3086
3284
|
const spec = generateOpenApiSpec(activeCollections, {
|
|
3087
3285
|
basePath,
|
|
@@ -3418,6 +3616,13 @@ function resolveBaseUrl(configured) {
|
|
|
3418
3616
|
function createTransport(config, environment) {
|
|
3419
3617
|
const fetchFn = config.fetch || globalThis.fetch;
|
|
3420
3618
|
const apiPath = config.apiPath || "/api";
|
|
3619
|
+
for (const field of ["baseUrl", "storageUrlOrigin"]) {
|
|
3620
|
+
const value = config[field];
|
|
3621
|
+
if (!value || !apiPath) continue;
|
|
3622
|
+
const trimmed = value.replace(/\/+$/, "");
|
|
3623
|
+
if (!trimmed.endsWith(apiPath)) continue;
|
|
3624
|
+
console.warn(`[Rebase] ${field} ${JSON.stringify(value)} already ends with the API path ${JSON.stringify(apiPath)}, which is appended to it — requests will go to ${trimmed}${apiPath}/… and 404. Pass the origin only (${JSON.stringify(trimmed.slice(0, trimmed.length - apiPath.length) || "/")}), or set \`apiPath\` if the server really does mount the API one level deeper.`);
|
|
3625
|
+
}
|
|
3421
3626
|
let token = config.token;
|
|
3422
3627
|
let tokenGetter;
|
|
3423
3628
|
let onUnauthorizedHandler = config.onUnauthorized;
|
|
@@ -3601,6 +3806,11 @@ function createAuth(transport, options) {
|
|
|
3601
3806
|
const authFlowMode = opts.authFlowMode || "json";
|
|
3602
3807
|
const STORAGE_KEY = "rebase_auth";
|
|
3603
3808
|
const REFRESH_BUFFER_MS = 12e4;
|
|
3809
|
+
/**
|
|
3810
|
+
* The largest delay `setTimeout` can hold — 2^31 - 1 ms, about 24.8 days.
|
|
3811
|
+
* Anything larger is silently clamped to 1ms by Node and every browser.
|
|
3812
|
+
*/
|
|
3813
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
3604
3814
|
const MAX_REFRESH_RETRIES = 5;
|
|
3605
3815
|
const REFRESH_RETRY_BASE_MS = 1e3;
|
|
3606
3816
|
const REFRESH_RETRY_MAX_MS = 3e4;
|
|
@@ -3628,7 +3838,9 @@ function createAuth(transport, options) {
|
|
|
3628
3838
|
function emit(event, session) {
|
|
3629
3839
|
for (const fn of listeners) try {
|
|
3630
3840
|
fn(event, session);
|
|
3631
|
-
} catch (e) {
|
|
3841
|
+
} catch (e) {
|
|
3842
|
+
console.error("Error in auth state change listener:", e);
|
|
3843
|
+
}
|
|
3632
3844
|
}
|
|
3633
3845
|
function saveSession(session) {
|
|
3634
3846
|
if (!persistSession || authFlowMode === "cookie") return;
|
|
@@ -3732,6 +3944,10 @@ function createAuth(transport, options) {
|
|
|
3732
3944
|
attemptScheduledRefresh(0);
|
|
3733
3945
|
return;
|
|
3734
3946
|
}
|
|
3947
|
+
if (delay > MAX_TIMER_DELAY_MS) {
|
|
3948
|
+
refreshTimeout = setTimeout(() => scheduleRefresh(expiresAt), MAX_TIMER_DELAY_MS);
|
|
3949
|
+
return;
|
|
3950
|
+
}
|
|
3735
3951
|
refreshTimeout = setTimeout(() => {
|
|
3736
3952
|
attemptScheduledRefresh(0);
|
|
3737
3953
|
}, delay);
|
|
@@ -4506,6 +4722,11 @@ var SDKQueryBuilder = class {
|
|
|
4506
4722
|
return this.collection.listen(this.params, onUpdate, onError);
|
|
4507
4723
|
}
|
|
4508
4724
|
};
|
|
4725
|
+
/**
|
|
4726
|
+
* Counts currently in flight, keyed by the exact request they issue. Entries
|
|
4727
|
+
* live only for the duration of the request — see `count()` for why.
|
|
4728
|
+
*/
|
|
4729
|
+
var inflightCounts = /* @__PURE__ */ new Map();
|
|
4509
4730
|
function createCollectionClient(transport, slug, ws) {
|
|
4510
4731
|
const basePath = `/data/${slug}`;
|
|
4511
4732
|
const client = {
|
|
@@ -4550,18 +4771,63 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
4550
4771
|
body: JSON.stringify({
|
|
4551
4772
|
rows: data,
|
|
4552
4773
|
...options?.upsert ? { upsert: true } : {}
|
|
4553
|
-
})
|
|
4774
|
+
}),
|
|
4775
|
+
...options?.idempotencyKey ? { headers: { "Idempotency-Key": options.idempotencyKey } } : {}
|
|
4554
4776
|
})).data || [];
|
|
4555
4777
|
},
|
|
4778
|
+
/**
|
|
4779
|
+
* Still `PUT`, deliberately, even though the server now serves `PATCH`
|
|
4780
|
+
* on the same handler and `PATCH` is the honest verb for a merge.
|
|
4781
|
+
*
|
|
4782
|
+
* The two are interchangeable server-side, so switching buys nothing at
|
|
4783
|
+
* runtime — and it costs compatibility in the direction that fails
|
|
4784
|
+
* quietly. A 0.14 client talking to a 0.13 server would send `PATCH` to
|
|
4785
|
+
* a route that does not exist and get a **404**, which is
|
|
4786
|
+
* indistinguishable from "that row is gone". Every write would look like
|
|
4787
|
+
* a missing record.
|
|
4788
|
+
*
|
|
4789
|
+
* `PATCH` is what the OpenAPI spec advertises, so anyone generating a
|
|
4790
|
+
* client gets the correct verb; this stays on `PUT` until the oldest
|
|
4791
|
+
* supported server is one that serves both.
|
|
4792
|
+
*/
|
|
4556
4793
|
async update(id, data) {
|
|
4557
4794
|
return await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, {
|
|
4558
4795
|
method: "PUT",
|
|
4559
4796
|
body: JSON.stringify(data)
|
|
4560
4797
|
});
|
|
4561
4798
|
},
|
|
4799
|
+
async updateMany(updates, options) {
|
|
4800
|
+
if (!Array.isArray(updates)) throw new TypeError("updateMany expects an array of { id, data } entries.");
|
|
4801
|
+
if (updates.length === 0) return [];
|
|
4802
|
+
return (await transport.request(`${basePath}/bulk`, {
|
|
4803
|
+
method: "PATCH",
|
|
4804
|
+
body: JSON.stringify({ updates }),
|
|
4805
|
+
...options?.idempotencyKey ? { headers: { "Idempotency-Key": options.idempotencyKey } } : {}
|
|
4806
|
+
})).data || [];
|
|
4807
|
+
},
|
|
4562
4808
|
async delete(id) {
|
|
4563
4809
|
await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, { method: "DELETE" });
|
|
4564
4810
|
},
|
|
4811
|
+
/**
|
|
4812
|
+
* `POST .../bulk/delete`, not `DELETE .../bulk`.
|
|
4813
|
+
*
|
|
4814
|
+
* The honest verb would take the ids in a DELETE body, and that is the
|
|
4815
|
+
* one request shape the HTTP ecosystem handles unreliably: bodies on
|
|
4816
|
+
* DELETE are permitted but widely dropped by proxies and CDNs, and
|
|
4817
|
+
* several OpenAPI generators ignore `requestBody` on a DELETE
|
|
4818
|
+
* operation, so a generated client would send the request without its
|
|
4819
|
+
* ids. A backend deployed behind arbitrary ingress cannot take that
|
|
4820
|
+
* bet. Same reason `:batchDelete` exists in Google's API guidelines.
|
|
4821
|
+
*/
|
|
4822
|
+
async deleteMany(ids, options) {
|
|
4823
|
+
if (!Array.isArray(ids)) throw new TypeError("deleteMany expects an array of ids.");
|
|
4824
|
+
if (ids.length === 0) return;
|
|
4825
|
+
await transport.request(`${basePath}/bulk/delete`, {
|
|
4826
|
+
method: "POST",
|
|
4827
|
+
body: JSON.stringify({ ids }),
|
|
4828
|
+
...options?.idempotencyKey ? { headers: { "Idempotency-Key": options.idempotencyKey } } : {}
|
|
4829
|
+
});
|
|
4830
|
+
},
|
|
4565
4831
|
async count(params) {
|
|
4566
4832
|
const qs = buildQueryString({
|
|
4567
4833
|
...params,
|
|
@@ -4569,12 +4835,28 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
4569
4835
|
offset: void 0,
|
|
4570
4836
|
include: void 0
|
|
4571
4837
|
});
|
|
4572
|
-
|
|
4838
|
+
const key = basePath + "/count" + qs;
|
|
4839
|
+
const inflight = inflightCounts.get(key);
|
|
4840
|
+
if (inflight) return inflight;
|
|
4841
|
+
const request = transport.request(key, { method: "GET" }).then((raw) => raw.count ?? 0);
|
|
4842
|
+
inflightCounts.set(key, request);
|
|
4843
|
+
try {
|
|
4844
|
+
return await request;
|
|
4845
|
+
} finally {
|
|
4846
|
+
inflightCounts.delete(key);
|
|
4847
|
+
}
|
|
4573
4848
|
},
|
|
4574
4849
|
observe(params, onResult, onError, options) {
|
|
4575
4850
|
let closed = false;
|
|
4576
|
-
|
|
4851
|
+
let liveDelivered = false;
|
|
4852
|
+
let signature;
|
|
4853
|
+
const deliver = (result, fromLive) => {
|
|
4577
4854
|
if (closed) return;
|
|
4855
|
+
if (fromLive) liveDelivered = true;
|
|
4856
|
+
else if (liveDelivered) return;
|
|
4857
|
+
const next = `${result.meta?.total ?? ""}|${JSON.stringify(result.data)}`;
|
|
4858
|
+
if (signature !== void 0 && next === signature) return;
|
|
4859
|
+
signature = next;
|
|
4578
4860
|
onResult({
|
|
4579
4861
|
...result,
|
|
4580
4862
|
fromCache: false,
|
|
@@ -4582,10 +4864,10 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
4582
4864
|
partial: false
|
|
4583
4865
|
});
|
|
4584
4866
|
};
|
|
4585
|
-
client.find(params).then(
|
|
4867
|
+
client.find(params).then((result) => deliver(result, false)).catch((error) => {
|
|
4586
4868
|
if (!closed) onError?.(error);
|
|
4587
4869
|
});
|
|
4588
|
-
const live = options?.realtime !== false && client.listen ? client.listen(params,
|
|
4870
|
+
const live = options?.realtime !== false && client.listen ? client.listen(params, (result) => deliver(result, true), onError) : void 0;
|
|
4589
4871
|
return () => {
|
|
4590
4872
|
closed = true;
|
|
4591
4873
|
live?.();
|
|
@@ -4593,17 +4875,24 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
4593
4875
|
},
|
|
4594
4876
|
observeById(id, onResult, onError, options) {
|
|
4595
4877
|
let closed = false;
|
|
4596
|
-
|
|
4878
|
+
let liveDelivered = false;
|
|
4879
|
+
let signature;
|
|
4880
|
+
const deliver = (row, fromLive) => {
|
|
4597
4881
|
if (closed) return;
|
|
4882
|
+
if (fromLive) liveDelivered = true;
|
|
4883
|
+
else if (liveDelivered) return;
|
|
4884
|
+
const next = row === void 0 ? "\0missing" : JSON.stringify(row);
|
|
4885
|
+
if (signature !== void 0 && next === signature) return;
|
|
4886
|
+
signature = next;
|
|
4598
4887
|
onResult(row, {
|
|
4599
4888
|
fromCache: false,
|
|
4600
4889
|
hasPendingWrites: false
|
|
4601
4890
|
});
|
|
4602
4891
|
};
|
|
4603
|
-
client.findById(id).then(
|
|
4892
|
+
client.findById(id).then((row) => deliver(row, false)).catch((error) => {
|
|
4604
4893
|
if (!closed) onError?.(error);
|
|
4605
4894
|
});
|
|
4606
|
-
const live = options?.realtime !== false && client.listenById ? client.listenById(id,
|
|
4895
|
+
const live = options?.realtime !== false && client.listenById ? client.listenById(id, (row) => deliver(row, true), onError) : void 0;
|
|
4607
4896
|
return () => {
|
|
4608
4897
|
closed = true;
|
|
4609
4898
|
live?.();
|
|
@@ -4634,51 +4923,43 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
4634
4923
|
client.listen = (params, onUpdate, onError) => {
|
|
4635
4924
|
let active = true;
|
|
4636
4925
|
let lastUpdateId = 0;
|
|
4926
|
+
let lastKnownTotal;
|
|
4927
|
+
const window = resolveFindWindow(params);
|
|
4637
4928
|
const unsub = ws.listenCollection({
|
|
4638
4929
|
path: slug,
|
|
4639
4930
|
filter: params?.where,
|
|
4931
|
+
logical: params?.logical,
|
|
4640
4932
|
limit: params?.limit,
|
|
4641
|
-
|
|
4933
|
+
offset: window.driverOffset,
|
|
4642
4934
|
orderBy: params?.orderBy?.[0],
|
|
4643
4935
|
order: params?.orderBy?.[1],
|
|
4644
4936
|
searchString: params?.searchString
|
|
4645
4937
|
}, (incomingRows) => {
|
|
4646
4938
|
const currentUpdateId = ++lastUpdateId;
|
|
4647
|
-
const requestedLimit =
|
|
4648
|
-
const offset =
|
|
4939
|
+
const requestedLimit = window.limit;
|
|
4940
|
+
const offset = window.offset;
|
|
4649
4941
|
const rows = incomingRows;
|
|
4650
|
-
const
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
if (active && currentUpdateId === lastUpdateId) onUpdate({
|
|
4942
|
+
const emit = (total, hasMore) => {
|
|
4943
|
+
if (!active || currentUpdateId !== lastUpdateId) return;
|
|
4944
|
+
onUpdate({
|
|
4654
4945
|
data: rows,
|
|
4655
4946
|
meta: {
|
|
4656
4947
|
total,
|
|
4657
4948
|
limit: requestedLimit,
|
|
4658
4949
|
offset,
|
|
4659
|
-
hasMore
|
|
4950
|
+
hasMore
|
|
4660
4951
|
}
|
|
4661
4952
|
});
|
|
4953
|
+
};
|
|
4954
|
+
const emitWithoutCount = () => emit(offset + rows.length, rows.length >= requestedLimit);
|
|
4955
|
+
if (client.count) client.count(params).then((total) => {
|
|
4956
|
+
lastKnownTotal = total;
|
|
4957
|
+
emit(total, offset + rows.length < total);
|
|
4662
4958
|
}).catch(() => {
|
|
4663
|
-
if (
|
|
4664
|
-
|
|
4665
|
-
meta: {
|
|
4666
|
-
total: heuristicTotal,
|
|
4667
|
-
limit: requestedLimit,
|
|
4668
|
-
offset,
|
|
4669
|
-
hasMore: heuristicHasMore
|
|
4670
|
-
}
|
|
4671
|
-
});
|
|
4672
|
-
});
|
|
4673
|
-
else onUpdate({
|
|
4674
|
-
data: rows,
|
|
4675
|
-
meta: {
|
|
4676
|
-
total: heuristicTotal,
|
|
4677
|
-
limit: requestedLimit,
|
|
4678
|
-
offset,
|
|
4679
|
-
hasMore: heuristicHasMore
|
|
4680
|
-
}
|
|
4959
|
+
if (lastKnownTotal !== void 0) emit(lastKnownTotal, offset + rows.length < lastKnownTotal);
|
|
4960
|
+
else emitWithoutCount();
|
|
4681
4961
|
});
|
|
4962
|
+
else emitWithoutCount();
|
|
4682
4963
|
}, onError);
|
|
4683
4964
|
return () => {
|
|
4684
4965
|
active = false;
|
|
@@ -6061,15 +6342,10 @@ var RebaseWebSocketClient = class {
|
|
|
6061
6342
|
}
|
|
6062
6343
|
}
|
|
6063
6344
|
createCollectionSubscriptionKey(props) {
|
|
6345
|
+
const { collection, ...query } = props;
|
|
6064
6346
|
const key = {
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
limit: props.limit,
|
|
6068
|
-
startAfter: props.startAfter,
|
|
6069
|
-
orderBy: props.orderBy,
|
|
6070
|
-
order: props.order,
|
|
6071
|
-
searchString: props.searchString,
|
|
6072
|
-
collection: props.collection?.name
|
|
6347
|
+
...query,
|
|
6348
|
+
collection: collection?.name
|
|
6073
6349
|
};
|
|
6074
6350
|
return JSON.stringify(key, (_, value) => {
|
|
6075
6351
|
if (value && typeof value === "object" && !Array.isArray(value)) return Object.keys(value).sort().reduce((sorted, k) => {
|
|
@@ -6982,14 +7258,23 @@ function looseEquals(a, b) {
|
|
|
6982
7258
|
*/
|
|
6983
7259
|
function likeToRegExp(pattern, caseInsensitive) {
|
|
6984
7260
|
let source = "^";
|
|
7261
|
+
let lastWasWildcard = false;
|
|
6985
7262
|
for (let i = 0; i < pattern.length; i++) {
|
|
6986
7263
|
const char = pattern[i];
|
|
6987
7264
|
if (char === "\\" && i + 1 < pattern.length) {
|
|
6988
7265
|
source += pattern[i + 1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6989
7266
|
i++;
|
|
6990
|
-
|
|
6991
|
-
else if (char === "
|
|
6992
|
-
|
|
7267
|
+
lastWasWildcard = false;
|
|
7268
|
+
} else if (char === "%") {
|
|
7269
|
+
if (!lastWasWildcard) source += "[\\s\\S]*";
|
|
7270
|
+
lastWasWildcard = true;
|
|
7271
|
+
} else if (char === "_") {
|
|
7272
|
+
source += "[\\s\\S]";
|
|
7273
|
+
lastWasWildcard = false;
|
|
7274
|
+
} else {
|
|
7275
|
+
source += char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7276
|
+
lastWasWildcard = false;
|
|
7277
|
+
}
|
|
6993
7278
|
}
|
|
6994
7279
|
return new RegExp(source + "$", caseInsensitive ? "i" : "");
|
|
6995
7280
|
}
|
|
@@ -7121,12 +7406,20 @@ function sortRows(rows, orderBy) {
|
|
|
7121
7406
|
function tiebreak(a, b) {
|
|
7122
7407
|
return compareValues(a.id, b.id) ?? 0;
|
|
7123
7408
|
}
|
|
7124
|
-
/**
|
|
7409
|
+
/**
|
|
7410
|
+
* Resolve `page`/`offset`/`limit` the way the server does.
|
|
7411
|
+
*
|
|
7412
|
+
* It did not: this defaulted an absent limit to 20 while `/api/data` pages by
|
|
7413
|
+
* 50, so the same `observe()` answered with 20 rows from the local database and
|
|
7414
|
+
* 50 from the network — a list that changed length depending on which side
|
|
7415
|
+
* answered, with `page` striding differently on each. Delegated now, so the
|
|
7416
|
+
* sentence above is true by construction rather than by agreement.
|
|
7417
|
+
*/
|
|
7125
7418
|
function resolvePagination(params) {
|
|
7126
|
-
const limit = params
|
|
7419
|
+
const { limit, offset } = resolveFindWindow(params);
|
|
7127
7420
|
return {
|
|
7128
7421
|
limit,
|
|
7129
|
-
offset
|
|
7422
|
+
offset
|
|
7130
7423
|
};
|
|
7131
7424
|
}
|
|
7132
7425
|
/**
|
|
@@ -7450,6 +7743,72 @@ var OfflineManager = class {
|
|
|
7450
7743
|
this.notifyCollection(slug);
|
|
7451
7744
|
return rows;
|
|
7452
7745
|
},
|
|
7746
|
+
updateMany: async (updates, options) => {
|
|
7747
|
+
await this.ensureCollection(slug);
|
|
7748
|
+
if (!Array.isArray(updates)) throw new TypeError("updateMany expects an array of { id, data } entries.");
|
|
7749
|
+
if (updates.length === 0) return [];
|
|
7750
|
+
const anyPending = updates.some((u) => this.hasPending(slug, u.id));
|
|
7751
|
+
if (this.connectivity.shouldAttempt() && !anyPending) try {
|
|
7752
|
+
const rows = await inner.updateMany(updates, options);
|
|
7753
|
+
this.connectivity.markSuccess();
|
|
7754
|
+
await this.ingest(slug, rows);
|
|
7755
|
+
this.notifyCollection(slug);
|
|
7756
|
+
return rows;
|
|
7757
|
+
} catch (error) {
|
|
7758
|
+
if (!isNetworkError(error)) throw error;
|
|
7759
|
+
this.connectivity.markFailure();
|
|
7760
|
+
}
|
|
7761
|
+
const rollback = {};
|
|
7762
|
+
const optimistic = [];
|
|
7763
|
+
for (const { id, data } of updates) {
|
|
7764
|
+
const base = this.rawLocalRow(slug, id);
|
|
7765
|
+
rollback[String(id)] = base ?? null;
|
|
7766
|
+
optimistic.push({
|
|
7767
|
+
...base ?? {},
|
|
7768
|
+
...data,
|
|
7769
|
+
id
|
|
7770
|
+
});
|
|
7771
|
+
}
|
|
7772
|
+
await this.enqueue({
|
|
7773
|
+
collection: slug,
|
|
7774
|
+
type: "updateMany",
|
|
7775
|
+
updates: updates.map((u) => ({
|
|
7776
|
+
id: u.id,
|
|
7777
|
+
data: u.data
|
|
7778
|
+
})),
|
|
7779
|
+
rollback: { rows: rollback }
|
|
7780
|
+
});
|
|
7781
|
+
for (const row of optimistic) this.setLocalRow(slug, row.id, row);
|
|
7782
|
+
this.notifyCollection(slug);
|
|
7783
|
+
return optimistic;
|
|
7784
|
+
},
|
|
7785
|
+
deleteMany: async (ids, options) => {
|
|
7786
|
+
await this.ensureCollection(slug);
|
|
7787
|
+
if (!Array.isArray(ids)) throw new TypeError("deleteMany expects an array of ids.");
|
|
7788
|
+
if (ids.length === 0) return;
|
|
7789
|
+
const anyPending = ids.some((id) => this.hasPending(slug, id));
|
|
7790
|
+
if (this.connectivity.shouldAttempt() && !anyPending) try {
|
|
7791
|
+
await inner.deleteMany(ids, options);
|
|
7792
|
+
this.connectivity.markSuccess();
|
|
7793
|
+
for (const id of ids) this.removeLocalRow(slug, id, true);
|
|
7794
|
+
this.notifyCollection(slug);
|
|
7795
|
+
this.scheduleRefresh(slug);
|
|
7796
|
+
return;
|
|
7797
|
+
} catch (error) {
|
|
7798
|
+
if (!isNetworkError(error)) throw error;
|
|
7799
|
+
this.connectivity.markFailure();
|
|
7800
|
+
}
|
|
7801
|
+
const rollback = {};
|
|
7802
|
+
for (const id of ids) rollback[String(id)] = this.rawLocalRow(slug, id) ?? null;
|
|
7803
|
+
await this.enqueue({
|
|
7804
|
+
collection: slug,
|
|
7805
|
+
type: "deleteMany",
|
|
7806
|
+
ids,
|
|
7807
|
+
rollback: { rows: rollback }
|
|
7808
|
+
});
|
|
7809
|
+
for (const id of ids) this.removeLocalRow(slug, id, false);
|
|
7810
|
+
this.notifyCollection(slug);
|
|
7811
|
+
},
|
|
7453
7812
|
update: async (id, data) => {
|
|
7454
7813
|
await this.ensureCollection(slug);
|
|
7455
7814
|
if (this.connectivity.shouldAttempt() && !this.hasPending(slug, id)) try {
|
|
@@ -7748,9 +8107,8 @@ var OfflineManager = class {
|
|
|
7748
8107
|
if (!state) return {
|
|
7749
8108
|
data: [],
|
|
7750
8109
|
meta: {
|
|
8110
|
+
...resolvePagination(params),
|
|
7751
8111
|
total: 0,
|
|
7752
|
-
limit: params?.limit ?? 20,
|
|
7753
|
-
offset: params?.offset ?? 0,
|
|
7754
8112
|
hasMore: false
|
|
7755
8113
|
},
|
|
7756
8114
|
fromCache: true,
|
|
@@ -7786,16 +8144,14 @@ var OfflineManager = class {
|
|
|
7786
8144
|
}
|
|
7787
8145
|
let added = 0;
|
|
7788
8146
|
const offset = snapshot.offset ?? 0;
|
|
7789
|
-
if (exact && offset === 0) {
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
added++;
|
|
7796
|
-
}
|
|
7797
|
-
if (added > 0 && params?.orderBy) sortRows(rows, params.orderBy);
|
|
8147
|
+
if (exact && offset === 0) for (const [key, entry] of state.rows) {
|
|
8148
|
+
if (seen.has(key) || !this.hasPending(slug, key)) continue;
|
|
8149
|
+
if (!this.isLocallyCreated(slug, key)) continue;
|
|
8150
|
+
if (!matchesParams(entry.row, params)) continue;
|
|
8151
|
+
rows.push(entry.row);
|
|
8152
|
+
added++;
|
|
7798
8153
|
}
|
|
8154
|
+
if (params?.orderBy) sortRows(rows, params.orderBy);
|
|
7799
8155
|
return {
|
|
7800
8156
|
data: rows,
|
|
7801
8157
|
meta: {
|
|
@@ -7942,17 +8298,17 @@ var OfflineManager = class {
|
|
|
7942
8298
|
return row;
|
|
7943
8299
|
}
|
|
7944
8300
|
recordSnapshot(slug, params, result) {
|
|
8301
|
+
const window = resolvePagination(params);
|
|
7945
8302
|
const meta = result.meta ?? {
|
|
7946
8303
|
total: result.data?.length ?? 0,
|
|
7947
|
-
|
|
7948
|
-
offset: 0,
|
|
8304
|
+
...window,
|
|
7949
8305
|
hasMore: false
|
|
7950
8306
|
};
|
|
7951
8307
|
const snapshot = {
|
|
7952
8308
|
ids: (result.data ?? []).map((row) => row.id).filter((id) => id !== void 0),
|
|
7953
8309
|
total: meta.total ?? result.data?.length ?? 0,
|
|
7954
|
-
limit: meta.limit ??
|
|
7955
|
-
offset: meta.offset ??
|
|
8310
|
+
limit: meta.limit ?? window.limit,
|
|
8311
|
+
offset: meta.offset ?? window.offset,
|
|
7956
8312
|
hasMore: meta.hasMore ?? false
|
|
7957
8313
|
};
|
|
7958
8314
|
const state = this.collectionState(slug);
|
|
@@ -8167,11 +8523,25 @@ var OfflineManager = class {
|
|
|
8167
8523
|
await this.adoptServerRow(op, op.id, row);
|
|
8168
8524
|
} else if (op.type === "createMany") {
|
|
8169
8525
|
const queued = op.data ?? [];
|
|
8170
|
-
const rows = await inner.createMany(queued,
|
|
8526
|
+
const rows = await inner.createMany(queued, {
|
|
8527
|
+
...op.upsert ? { upsert: true } : {},
|
|
8528
|
+
idempotencyKey: op.mutationId
|
|
8529
|
+
});
|
|
8171
8530
|
for (let i = 0; i < rows.length; i++) await this.adoptServerRow(op, queued[i]?.id, rows[i]);
|
|
8531
|
+
} else if (op.type === "updateMany") {
|
|
8532
|
+
const queued = op.updates ?? [];
|
|
8533
|
+
const rows = await inner.updateMany(queued.map((u) => ({
|
|
8534
|
+
id: u.id,
|
|
8535
|
+
data: u.data
|
|
8536
|
+
})), { idempotencyKey: op.mutationId });
|
|
8537
|
+
for (let i = 0; i < rows.length; i++) await this.ingestReplaced(op, queued[i].id, rows[i]);
|
|
8172
8538
|
} else if (op.type === "update") {
|
|
8173
8539
|
const row = await inner.update(op.id, op.data);
|
|
8174
8540
|
await this.ingestReplaced(op, op.id, row);
|
|
8541
|
+
} else if (op.type === "deleteMany") {
|
|
8542
|
+
const ids = op.ids ?? [];
|
|
8543
|
+
await inner.deleteMany(ids, { idempotencyKey: op.mutationId });
|
|
8544
|
+
for (const id of ids) this.removeLocalRow(op.collection, id, true);
|
|
8175
8545
|
} else if (op.type === "delete") {
|
|
8176
8546
|
await inner.delete(op.id);
|
|
8177
8547
|
this.removeLocalRow(op.collection, op.id, true);
|
|
@@ -8414,23 +8784,37 @@ var OfflineManager = class {
|
|
|
8414
8784
|
/**
|
|
8415
8785
|
* Derive a WebSocket URL from an HTTP base URL.
|
|
8416
8786
|
* `http://` → `ws://`, `https://` → `wss://`.
|
|
8787
|
+
*
|
|
8788
|
+
* A backend mounted under a path is the reason `baseUrl` accepts one, so the
|
|
8789
|
+
* path is kept. It used to be kept for an absolute `baseUrl` and dropped for a
|
|
8790
|
+
* relative one — resolved through `.origin` — so one deployment dialled two
|
|
8791
|
+
* different sockets depending on whether its config said `"/backend"` or
|
|
8792
|
+
* `"https://app.example.com/backend"`.
|
|
8793
|
+
*
|
|
8794
|
+
* Returns `""` when there is nothing to resolve against: a relative `baseUrl`
|
|
8795
|
+
* outside a browser has no origin, and inventing one would dial somewhere
|
|
8796
|
+
* arbitrary. The caller warns rather than leaving that silent.
|
|
8417
8797
|
*/
|
|
8418
8798
|
function deriveWebSocketUrl(baseUrl) {
|
|
8799
|
+
const toWsProtocol = (url) => {
|
|
8800
|
+
const secure = /^(https|wss):/i.test(url);
|
|
8801
|
+
return url.replace(/^https?:\/\//i, secure ? "wss://" : "ws://").replace(/^wss?:\/\//i, secure ? "wss://" : "ws://").replace(/\/$/, "");
|
|
8802
|
+
};
|
|
8419
8803
|
if (typeof window !== "undefined") {
|
|
8420
|
-
let absoluteUrl
|
|
8804
|
+
let absoluteUrl;
|
|
8421
8805
|
if (!baseUrl) absoluteUrl = window.location.origin;
|
|
8422
8806
|
else if (/^https?:\/\//i.test(baseUrl) || /^wss?:\/\//i.test(baseUrl)) absoluteUrl = baseUrl;
|
|
8423
8807
|
else try {
|
|
8424
|
-
|
|
8808
|
+
const resolved = new URL(baseUrl, window.location.href);
|
|
8809
|
+
absoluteUrl = resolved.origin + resolved.pathname;
|
|
8425
8810
|
} catch {
|
|
8426
8811
|
absoluteUrl = window.location.origin;
|
|
8427
8812
|
}
|
|
8428
|
-
|
|
8429
|
-
return absoluteUrl.replace(/^https?:\/\//i, `${protocol}//`).replace(/^wss?:\/\//i, `${protocol}//`).replace(/\/$/, "");
|
|
8813
|
+
return toWsProtocol(absoluteUrl);
|
|
8430
8814
|
}
|
|
8431
8815
|
if (!baseUrl) return "";
|
|
8432
8816
|
if (!/^https?:\/\//i.test(baseUrl) && !/^wss?:\/\//i.test(baseUrl)) return "";
|
|
8433
|
-
return baseUrl
|
|
8817
|
+
return toWsProtocol(baseUrl);
|
|
8434
8818
|
}
|
|
8435
8819
|
function createRebaseClient(options) {
|
|
8436
8820
|
const transport = createTransport(options, { credentialOutOfBand: options.auth?.authFlowMode === "cookie" });
|
|
@@ -8458,7 +8842,11 @@ function createRebaseClient(options) {
|
|
|
8458
8842
|
});
|
|
8459
8843
|
return storageSourcesPromise;
|
|
8460
8844
|
};
|
|
8461
|
-
const
|
|
8845
|
+
const realtimeEnabled = options.realtime !== false;
|
|
8846
|
+
const resolvedWsUrl = realtimeEnabled ? options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl) : void 0;
|
|
8847
|
+
const realtimeUnreachable = realtimeEnabled && !resolvedWsUrl;
|
|
8848
|
+
const unreachableReason = `no WebSocket URL could be derived from baseUrl ${JSON.stringify(options.baseUrl ?? null)} — outside a browser there is no page origin to resolve a relative URL against. Pass an absolute \`baseUrl\`, set \`websocketUrl\` explicitly, or pass \`realtime: false\` to say this was intended.`;
|
|
8849
|
+
if (realtimeUnreachable) console.warn(`[Rebase] Realtime is enabled but ${unreachableReason} Live queries will fall back to a single fetch and channels will throw.`);
|
|
8462
8850
|
let ws;
|
|
8463
8851
|
/** One channel object per name — see `realtime.channel`. */
|
|
8464
8852
|
const realtimeChannels = /* @__PURE__ */ new Map();
|
|
@@ -8578,7 +8966,7 @@ function createRebaseClient(options) {
|
|
|
8578
8966
|
* cut off the others.
|
|
8579
8967
|
*/
|
|
8580
8968
|
channel: (name, options) => {
|
|
8581
|
-
if (!ws) throw new RebaseClientError("Realtime is disabled on this client (realtime: false), so channels are unavailable.");
|
|
8969
|
+
if (!ws) throw new RebaseClientError(realtimeUnreachable ? `Realtime is enabled but ${unreachableReason}` : "Realtime is disabled on this client (realtime: false), so channels are unavailable.");
|
|
8582
8970
|
let existing = realtimeChannels.get(name);
|
|
8583
8971
|
if (!existing) {
|
|
8584
8972
|
existing = new RebaseRealtimeChannel(name, ws, options);
|
|
@@ -9249,7 +9637,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
9249
9637
|
]) {
|
|
9250
9638
|
const providerConfig = safeAuthConfig[key];
|
|
9251
9639
|
if (providerConfig && requiredFields.every((f) => Boolean(providerConfig[f]))) {
|
|
9252
|
-
const createFn = (await import("./auth-
|
|
9640
|
+
const createFn = (await import("./auth-CtxXQYV_.js").then((n) => n.t))[factory];
|
|
9253
9641
|
oauthProviders.push(createFn(providerConfig));
|
|
9254
9642
|
}
|
|
9255
9643
|
}
|
|
@@ -9343,7 +9731,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
9343
9731
|
let schemaEditorOff = schemaEditorUnavailable();
|
|
9344
9732
|
let schemaEditorRoutes;
|
|
9345
9733
|
if (!schemaEditorOff && config.collectionsDir) try {
|
|
9346
|
-
schemaEditorRoutes = (await import("./schema-editor-routes-
|
|
9734
|
+
schemaEditorRoutes = (await import("./schema-editor-routes-BqjzvvWU.js")).createSchemaEditorRoutes(config.collectionsDir);
|
|
9347
9735
|
} catch (err) {
|
|
9348
9736
|
if (err?.code === "ERR_MODULE_NOT_FOUND") {
|
|
9349
9737
|
schemaEditorOff = {
|
|
@@ -9513,7 +9901,8 @@ async function _initializeRebaseBackend(config) {
|
|
|
9513
9901
|
Object.assign(serverClient, { sql: (query, options) => driverAdmin.executeSql(query, options) });
|
|
9514
9902
|
logger.info("SQL capability attached to singleton");
|
|
9515
9903
|
}
|
|
9516
|
-
|
|
9904
|
+
const serverSingleton = serverClient;
|
|
9905
|
+
_initRebase(serverSingleton);
|
|
9517
9906
|
logger.info("Rebase singleton initialized");
|
|
9518
9907
|
if (defaultDriverResult.internals) {
|
|
9519
9908
|
const driver = defaultDriverResult.internals.driver;
|
|
@@ -9556,12 +9945,12 @@ async function _initializeRebaseBackend(config) {
|
|
|
9556
9945
|
let cronScheduler;
|
|
9557
9946
|
if (config.cronsDir) {
|
|
9558
9947
|
const { loadCronJobsFromDirectory } = await import("./cron-loader-B1S2MCSl.js").then((n) => n.t);
|
|
9559
|
-
const { CronScheduler } = await import("./cron-scheduler-
|
|
9560
|
-
const { createCronRoutes } = await import("./cron-routes-
|
|
9561
|
-
const { createCronStore } = await import("./cron-store-
|
|
9948
|
+
const { CronScheduler } = await import("./cron-scheduler-CsQtYLMq.js").then((n) => n.n);
|
|
9949
|
+
const { createCronRoutes } = await import("./cron-routes-D5a9v1HY.js").then((n) => n.n);
|
|
9950
|
+
const { createCronStore } = await import("./cron-store-CHH7zrif.js").then((n) => n.n);
|
|
9562
9951
|
const loadedCronJobs = await loadCronJobsFromDirectory(config.cronsDir);
|
|
9563
9952
|
cronScheduler = new CronScheduler();
|
|
9564
|
-
cronScheduler.setClient(
|
|
9953
|
+
cronScheduler.setClient(serverSingleton);
|
|
9565
9954
|
if (loadedCronJobs.length > 0) {
|
|
9566
9955
|
cronScheduler.registerJobs(loadedCronJobs);
|
|
9567
9956
|
const store = defaultBootstrapper.getAdmin?.(defaultDriverResult) && config.cronPersistence !== false ? createCronStore(defaultDriver) : void 0;
|
|
@@ -9583,7 +9972,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
9583
9972
|
} else logger.warn(`Cron routes mounted at ${basePath}/cron, but no jobs loaded from ${config.cronsDir}. Nothing is scheduled — check the messages above for files that failed to load.`);
|
|
9584
9973
|
}
|
|
9585
9974
|
{
|
|
9586
|
-
const { createBackupRoutes, parseBackupDestination } = await import("./backup-
|
|
9975
|
+
const { createBackupRoutes, parseBackupDestination } = await import("./backup-BJiY_mM-.js").then((n) => n.t);
|
|
9587
9976
|
const backupRouter = new Hono();
|
|
9588
9977
|
applyAdminGate(backupRouter, "Backup");
|
|
9589
9978
|
backupRouter.route("/", createBackupRoutes({
|
|
@@ -9605,7 +9994,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
9605
9994
|
logger.info("Logs routes mounted", { path: `${basePath}/logs` });
|
|
9606
9995
|
}
|
|
9607
9996
|
{
|
|
9608
|
-
const { createContractRoutes } = await import("./contract-routes-
|
|
9997
|
+
const { createContractRoutes } = await import("./contract-routes-FSt0Nyfk.js").then((n) => n.t);
|
|
9609
9998
|
const contractRouter = new Hono();
|
|
9610
9999
|
if (adminSurfacesGated) {
|
|
9611
10000
|
if (apiKeyPreAuth) contractRouter.use("/contract", apiKeyPreAuth);
|
|
@@ -9699,7 +10088,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
9699
10088
|
* export default defineFunction((app, { rebase }) => {
|
|
9700
10089
|
* app.use("/*", requireAuth);
|
|
9701
10090
|
* app.get("/home", async (c) => {
|
|
9702
|
-
* const [stats] = await rebase.sql
|
|
10091
|
+
* const [stats] = await rebase.sql(`SELECT count(*) AS n FROM orders`);
|
|
9703
10092
|
* return c.json({ orders: Number(stats.n) });
|
|
9704
10093
|
* });
|
|
9705
10094
|
* });
|
|
@@ -9731,12 +10120,14 @@ function defineFunction(definition) {
|
|
|
9731
10120
|
* export default defineCron({
|
|
9732
10121
|
* name: "Nightly cleanup",
|
|
9733
10122
|
* schedule: "0 3 * * *",
|
|
9734
|
-
* async handler({
|
|
9735
|
-
*
|
|
10123
|
+
* async handler({ rebase, log }) {
|
|
10124
|
+
* // `dataAsAdmin` bypasses RLS. A cron has no per-request user, so
|
|
10125
|
+
* // there is no policy to fall back on — scope the filters yourself.
|
|
10126
|
+
* const { data: expired } = await rebase.dataAsAdmin.sessions.find({
|
|
9736
10127
|
* where: { expired: ["==", true] },
|
|
9737
10128
|
* });
|
|
9738
10129
|
* for (const session of expired) {
|
|
9739
|
-
* await
|
|
10130
|
+
* await rebase.dataAsAdmin.sessions.delete(session.id as string);
|
|
9740
10131
|
* }
|
|
9741
10132
|
* log(`Deleted ${expired.length} expired sessions`);
|
|
9742
10133
|
* },
|
|
@@ -9749,36 +10140,41 @@ function defineCron(definition) {
|
|
|
9749
10140
|
//#endregion
|
|
9750
10141
|
//#region src/utils/sql.ts
|
|
9751
10142
|
/**
|
|
9752
|
-
* Returns a SQL chunk calling `
|
|
9753
|
-
* This is a PostgreSQL RLS helper function created in the `
|
|
10143
|
+
* Returns a SQL chunk calling `rebase.uid()` — the current user's ID.
|
|
10144
|
+
* This is a PostgreSQL RLS helper function created in the `rebase` schema
|
|
9754
10145
|
* that reads `app.uid` set per-transaction by `withAuth()`.
|
|
9755
10146
|
*
|
|
10147
|
+
* The function moved from `auth.uid()` to `rebase.uid()` in 1.0 — `auth` is
|
|
10148
|
+
* Supabase's schema, and taking it meant Rebase could not be pointed at a
|
|
10149
|
+
* database that already had one. Use this helper rather than writing the call
|
|
10150
|
+
* by hand and the move costs you nothing.
|
|
10151
|
+
*
|
|
9756
10152
|
* @example
|
|
9757
10153
|
* sql`${table.uid} = ${authUid()}`
|
|
9758
10154
|
*/
|
|
9759
10155
|
var authUid = () => {
|
|
9760
|
-
return sql
|
|
10156
|
+
return sql.raw(RLS_UID_SQL);
|
|
9761
10157
|
};
|
|
9762
10158
|
/**
|
|
9763
|
-
* Returns a SQL chunk calling `
|
|
10159
|
+
* Returns a SQL chunk calling `rebase.roles()` — the current user's roles
|
|
9764
10160
|
* as a comma-separated string.
|
|
9765
10161
|
* Reads `app.user_roles` set per-transaction by `withAuth()`.
|
|
9766
10162
|
*
|
|
9767
10163
|
* @example
|
|
9768
|
-
* sql
|
|
10164
|
+
* sql`${authRoles()} ~ 'admin'`
|
|
9769
10165
|
*/
|
|
9770
10166
|
var authRoles = () => {
|
|
9771
|
-
return sql
|
|
10167
|
+
return sql.raw(RLS_ROLES_SQL);
|
|
9772
10168
|
};
|
|
9773
10169
|
/**
|
|
9774
|
-
* Returns a SQL chunk calling `
|
|
10170
|
+
* Returns a SQL chunk calling `rebase.jwt()` — the full JWT claims as JSONB.
|
|
9775
10171
|
* Reads `app.jwt` set per-transaction by `withAuth()`.
|
|
9776
10172
|
*
|
|
9777
10173
|
* @example
|
|
9778
|
-
* sql
|
|
10174
|
+
* sql`${authJwt()}->>'sub'`
|
|
9779
10175
|
*/
|
|
9780
10176
|
var authJwt = () => {
|
|
9781
|
-
return sql
|
|
10177
|
+
return sql.raw(RLS_JWT_SQL);
|
|
9782
10178
|
};
|
|
9783
10179
|
//#endregion
|
|
9784
10180
|
//#region src/env.ts
|
|
@@ -9951,8 +10347,10 @@ var WebhookDispatcher = class {
|
|
|
9951
10347
|
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
|
|
9952
10348
|
const result = await this.deliver(webhook, event, payload, attempt);
|
|
9953
10349
|
if (result.success) return result;
|
|
9954
|
-
if (attempt < this.maxRetries)
|
|
9955
|
-
|
|
10350
|
+
if (attempt < this.maxRetries) {
|
|
10351
|
+
const backoff = this.retryDelays[attempt - 1] ?? this.retryDelays[this.retryDelays.length - 1];
|
|
10352
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
10353
|
+
} else return result;
|
|
9956
10354
|
}
|
|
9957
10355
|
return {
|
|
9958
10356
|
webhookId: webhook.id,
|
|
@@ -11785,7 +12183,8 @@ async function bootFromBundle(options = {}) {
|
|
|
11785
12183
|
});
|
|
11786
12184
|
await ensureCollectionPolicies(bundle, dataSources, env);
|
|
11787
12185
|
metrics?.setKnownCollections(backend.collectionRegistry.getCollections().map((collection) => collection.slug).filter((slug) => Boolean(slug)));
|
|
11788
|
-
|
|
12186
|
+
const healthPaths = ["/health", `${(env.REBASE_BASE_PATH || "/api").replace(/\/$/, "")}/health`];
|
|
12187
|
+
for (const healthPath of [...new Set(healthPaths)]) app.get(healthPath, async (c) => {
|
|
11789
12188
|
const result = await backend.healthCheck();
|
|
11790
12189
|
const unhealthy = (await Promise.all(dataSources.filter((source) => source.key !== DEFAULT_DATA_SOURCE_KEY).map(async (source) => ({
|
|
11791
12190
|
key: source.key,
|
|
@@ -11891,7 +12290,7 @@ async function bootStaticApp(bundle, devRoot, options) {
|
|
|
11891
12290
|
if (metrics) app.use("/*", metrics.middleware);
|
|
11892
12291
|
const server = createServer(getRequestListener(app.fetch));
|
|
11893
12292
|
app.get("/livez", (c) => c.json({ status: "ok" }));
|
|
11894
|
-
|
|
12293
|
+
for (const healthPath of [.../* @__PURE__ */ new Set(["/health", `${basePath.replace(/\/$/, "")}/health`])]) app.get(healthPath, (c) => c.json({
|
|
11895
12294
|
status: "ok",
|
|
11896
12295
|
latencyMs: 0
|
|
11897
12296
|
}));
|
|
@@ -12001,6 +12400,40 @@ function warnOnDriverSkew(dataSources, runtimeVersion) {
|
|
|
12001
12400
|
}
|
|
12002
12401
|
}
|
|
12003
12402
|
/**
|
|
12403
|
+
* The collections a data source's engine is the store for.
|
|
12404
|
+
*
|
|
12405
|
+
* A bundle's collections directory holds *every* collection the project
|
|
12406
|
+
* declares, whatever engine serves it — that is the point of `dataSource`
|
|
12407
|
+
* routing. What it is not is a list of tables to create: handing the whole
|
|
12408
|
+
* directory to the primary source's bootstrapper made a Firestore collection
|
|
12409
|
+
* declared alongside the Postgres ones arrive as an empty Postgres table, with
|
|
12410
|
+
* RLS policies, while the app went on reading its documents from Firestore.
|
|
12411
|
+
*
|
|
12412
|
+
* Excluding is deliberately conservative. A collection that names neither an
|
|
12413
|
+
* `engine` nor a `dataSource` belongs to whichever source is primary — the
|
|
12414
|
+
* "postgres" that `resolveDataSource` falls back to there is a default, not a
|
|
12415
|
+
* declaration, and must not exclude anything on its own. Only a collection that
|
|
12416
|
+
* explicitly routes to a *different* engine is dropped, so a project running two
|
|
12417
|
+
* sources on the same engine is unaffected.
|
|
12418
|
+
*/
|
|
12419
|
+
function collectionsStoredBy(collections, primary, dataSources) {
|
|
12420
|
+
const registry = createDataSourceRegistry(dataSources.map((source) => ({
|
|
12421
|
+
key: source.key,
|
|
12422
|
+
engine: source.engine
|
|
12423
|
+
})));
|
|
12424
|
+
return collections.filter((collection) => {
|
|
12425
|
+
const declared = collection.engine ?? (collection.dataSource ? resolveDataSource(collection, registry).engine : void 0);
|
|
12426
|
+
if (!declared) return true;
|
|
12427
|
+
return declared === primary.engine;
|
|
12428
|
+
});
|
|
12429
|
+
}
|
|
12430
|
+
/** Say what was routed elsewhere, so a missing table is never a silent one. */
|
|
12431
|
+
function logForeignCollections(all, stored, primary) {
|
|
12432
|
+
if (stored.length === all.length) return;
|
|
12433
|
+
const foreign = all.filter((c) => !stored.includes(c)).map((c) => c.slug);
|
|
12434
|
+
logger.info(`Skipping ${foreign.length} collection(s) served by another engine, not "${primary.engine}": ${foreign.join(", ")}. Their storage is not managed by this data source.`);
|
|
12435
|
+
}
|
|
12436
|
+
/**
|
|
12004
12437
|
* Bring the database's collection tables up to date before serving.
|
|
12005
12438
|
*
|
|
12006
12439
|
* Delegates to whichever driver bootstrapped the default data source; a driver
|
|
@@ -12049,11 +12482,17 @@ async function ensureCollectionSchema(bundle, dataSources, env) {
|
|
|
12049
12482
|
skip(`the adapter from "${primary.driverPackage}" (engine "${primary.engine}") does not expose collection-table creation. ` + (skew.detail ? `${skew.detail} ` : "") + "The driver package may well implement it on a class the adapter does not forward, so check the adapter's shape before blaming its version. Collection tables will NOT be created, so every /api/data route will fail on a missing relation.\n" + schemaRecoveryGuidance({ staleDriver: skew.stale }), "warn");
|
|
12050
12483
|
return;
|
|
12051
12484
|
}
|
|
12052
|
-
const
|
|
12053
|
-
if (
|
|
12485
|
+
const loaded = await loadCollectionsFromDirectory(bundle.collectionsDir);
|
|
12486
|
+
if (loaded.length === 0) {
|
|
12054
12487
|
skip(`no collections were loaded from "${bundle.collectionsDir}".`, "warn");
|
|
12055
12488
|
return;
|
|
12056
12489
|
}
|
|
12490
|
+
const collections = collectionsStoredBy(loaded, primary, dataSources);
|
|
12491
|
+
logForeignCollections(loaded, collections, primary);
|
|
12492
|
+
if (collections.length === 0) {
|
|
12493
|
+
skip(`none of the ${loaded.length} declared collection(s) are stored by the "${primary.engine}" data source.`);
|
|
12494
|
+
return;
|
|
12495
|
+
}
|
|
12057
12496
|
const { applied } = await primary.bootstrapper.ensureCollectionSchema(collections, preInitDriverResult(primary), (message) => logger.info(`schema: ${message}`));
|
|
12058
12497
|
logger.info(applied > 0 ? `Applied ${applied} additive schema change(s) before boot.` : "Collection schema is up to date.");
|
|
12059
12498
|
}
|
|
@@ -12103,12 +12542,14 @@ async function ensureCollectionPolicies(bundle, dataSources, env) {
|
|
|
12103
12542
|
logger.warn(`Collection policies: skipped — the "${primary.driverPackage}" driver (engine "${primary.engine}") does not apply RLS policies at boot. ` + (skew.detail ? `${skew.detail} ` : "") + "Collections will deny reads until policies are applied.\n" + schemaRecoveryGuidance({ staleDriver: skew.stale }));
|
|
12104
12543
|
return;
|
|
12105
12544
|
}
|
|
12106
|
-
const
|
|
12545
|
+
const loaded = await loadCollectionsFromDirectory(bundle.collectionsDir);
|
|
12546
|
+
if (loaded.length === 0) return;
|
|
12547
|
+
const collections = collectionsStoredBy(loaded, primary, dataSources);
|
|
12107
12548
|
if (collections.length === 0) return;
|
|
12108
12549
|
const { applied } = await primary.bootstrapper.ensureCollectionPolicies(collections, preInitDriverResult(primary), (message) => logger.info(`policies: ${message}`));
|
|
12109
12550
|
logger.info(applied > 0 ? `Applied ${applied} RLS policy statement(s) before serving.` : "RLS policies are up to date.");
|
|
12110
12551
|
}
|
|
12111
12552
|
//#endregion
|
|
12112
|
-
export { ApiError, BundleError, CronScheduler, DEFAULT_DRIVER_ID, DEFAULT_MAX_FILE_SIZE, DEFAULT_STORAGE_ID, DOCUMENT_MIME_TYPES, DefaultDriverRegistry, DefaultStorageRegistry, GCSStorageController, IMAGE_MIME_TYPES, LocalStorageController, MetricsRegistry, S3StorageController, SMTPEmailService, TransformCache, TusHandler, WebhookDispatcher, _resetRebaseMock, _setRebaseMock, applyCollectionDefaults, assertCollectionConfigs, assertDistinctSuffixes, authJwt, authRoles, authUid, bootFromBundle, classifySurface, cleanupDevPortFile, createAppleProvider, createBackupRoutes, createBitbucketProvider, createBuiltinAuthAdapter, createContractRoutes, createCronRoutes, createCronStore, createCustomAuthAdapter, createDiscordProvider, createEmailService, createFacebookProvider, createFunctionRoutes, createGitHubProvider, createGitLabProvider, createGoogleProvider, createHistoryRoutes, createLinkedinProvider, createMetricsMiddleware, createMetricsRoutes, createMicrosoftProvider, createSlackProvider, createSourceBundle, createSpotifyProvider, createStorageController, createStorageRoutes, createTwitterProvider, defineCron, defineFunction, envSuffixForKey, errorHandler, extractUserFromToken, fileTokenAuth, findCollectionConfigProblems, generateSecurePassword, getEmailVerificationTemplate, getMagicLinkTemplate, getPasswordResetTemplate, getUserInvitationTemplate, getWelcomeEmailTemplate, hashPassword, httpMethodToOperation, initializeDataSource, initializeDataSources, initializeRebaseBackend, installShutdownHandlers, isApiKeyToken, isAuthAdapter, isDatabaseAdapter, isLocalhostOrigin, isOperationAllowed,
|
|
12553
|
+
export { ApiError, BundleError, CronScheduler, DEFAULT_DRIVER_ID, DEFAULT_MAX_FILE_SIZE, DEFAULT_STORAGE_ID, DOCUMENT_MIME_TYPES, DefaultDriverRegistry, DefaultStorageRegistry, GCSStorageController, IMAGE_MIME_TYPES, LocalStorageController, MetricsRegistry, S3StorageController, SMTPEmailService, TransformCache, TusHandler, WebhookDispatcher, _resetRebaseMock, _setRebaseMock, applyCollectionDefaults, assertCollectionConfigs, assertDistinctSuffixes, authJwt, authRoles, authUid, bootFromBundle, classifySurface, cleanupDevPortFile, createAppleProvider, createBackupRoutes, createBitbucketProvider, createBuiltinAuthAdapter, createContractRoutes, createCronRoutes, createCronStore, createCustomAuthAdapter, createDiscordProvider, createEmailService, createFacebookProvider, createFunctionRoutes, createGitHubProvider, createGitLabProvider, createGoogleProvider, createHistoryRoutes, createLinkedinProvider, createMetricsMiddleware, createMetricsRoutes, createMicrosoftProvider, createSlackProvider, createSourceBundle, createSpotifyProvider, createStorageController, createStorageRoutes, createTwitterProvider, defineCron, defineFunction, envSuffixForKey, errorHandler, extractUserFromToken, fileTokenAuth, findCollectionConfigProblems, generateSecurePassword, getEmailVerificationTemplate, getMagicLinkTemplate, getPasswordResetTemplate, getUserInvitationTemplate, getWelcomeEmailTemplate, hashPassword, httpMethodToOperation, initializeDataSource, initializeDataSources, initializeRebaseBackend, installShutdownHandlers, isApiKeyToken, isAuthAdapter, isDatabaseAdapter, isLocalhostOrigin, isOperationAllowed, isTransformableImage, listBackupObjects, listenWithPortRetry, loadBootEnv, loadBundle, loadBundleConfigExports, loadBundleSchema, loadCollectionsFromDirectory, loadCronJobsFromDirectory, loadDeclaredStorageSources, loadEnv, loadFunctionsFromDirectory, loadUsersCollection, logger, optionalAuth, parseBackupDestination, parseBackupTimestamp, parseTransformOptions, queryTokenAuth, readBackupBytes, readBundleManifest, rebase, requireAdmin, requireAuth, resolveAuthHooks, resolveAuthOptions, resolveCorsOrigin, resolveDataSources, resolveEmailOptions, resolveRequireAuth, resolveStorageBackend, resolveStorageSources, runFromBundle, safeCompare, serveSPA, transformImage, unknownKeyPolicyFromEnv, validateApiKey, validateCronExpression, validatePasswordStrength, verifyPassword };
|
|
12113
12554
|
|
|
12114
12555
|
//# sourceMappingURL=index.es.js.map
|