@rebasepro/types 0.17.3 → 0.18.1
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/README.md +4 -0
- package/dist/call_context.d.ts +20 -0
- package/dist/controllers/client.d.ts +36 -4
- package/dist/controllers/data.d.ts +120 -10
- package/dist/errors.d.ts +83 -4
- package/dist/index.es.js +522 -160
- package/dist/index.es.js.map +1 -1
- package/dist/types/admin_block.d.ts +2 -2
- package/dist/types/auth_adapter.d.ts +41 -6
- package/dist/types/backend.d.ts +48 -0
- package/dist/types/collections.d.ts +25 -1
- package/dist/types/cron.d.ts +34 -0
- package/dist/types/database_adapter.d.ts +39 -0
- package/dist/types/entity_callbacks.d.ts +14 -1
- package/dist/types/filter-operators.d.ts +24 -1
- package/dist/types/policy.d.ts +29 -1
- package/dist/types/properties.d.ts +216 -3
- package/dist/types/relations.d.ts +65 -7
- package/dist/types/resource_kinds.d.ts +173 -17
- package/dist/types/resources.d.ts +108 -7
- package/dist/types/rls-functions.d.ts +11 -0
- package/dist/types/storage_source.d.ts +12 -23
- package/package.json +24 -23
- package/src/call_context.ts +0 -120
- package/src/controllers/auth_state.ts +0 -24
- package/src/controllers/client.ts +0 -494
- package/src/controllers/collection_registry.ts +0 -62
- package/src/controllers/data.ts +0 -1012
- package/src/controllers/data_driver.ts +0 -576
- package/src/controllers/effective_role.ts +0 -4
- package/src/controllers/email.ts +0 -91
- package/src/controllers/index.ts +0 -11
- package/src/controllers/storage.ts +0 -252
- package/src/errors.ts +0 -119
- package/src/index.ts +0 -5
- package/src/types/admin_block.ts +0 -209
- package/src/types/api_keys.ts +0 -108
- package/src/types/auth_adapter.ts +0 -580
- package/src/types/backend.ts +0 -987
- package/src/types/backup.ts +0 -26
- package/src/types/channel_bus.ts +0 -202
- package/src/types/chips.ts +0 -34
- package/src/types/collection_contract.ts +0 -278
- package/src/types/collections.ts +0 -763
- package/src/types/component_ref.ts +0 -92
- package/src/types/cron.ts +0 -213
- package/src/types/data_source.ts +0 -357
- package/src/types/database_adapter.ts +0 -267
- package/src/types/entities.ts +0 -226
- package/src/types/entity_callbacks.ts +0 -229
- package/src/types/filter-operators.ts +0 -444
- package/src/types/history.ts +0 -66
- package/src/types/index.ts +0 -36
- package/src/types/indexes.ts +0 -180
- package/src/types/policy.ts +0 -328
- package/src/types/postgres_introspection.ts +0 -101
- package/src/types/project_manifest.ts +0 -598
- package/src/types/properties.ts +0 -1368
- package/src/types/relations.ts +0 -417
- package/src/types/resource_kinds.ts +0 -390
- package/src/types/resources.ts +0 -368
- package/src/types/rls-functions.ts +0 -98
- package/src/types/schema_editing.ts +0 -157
- package/src/types/schema_version.ts +0 -112
- package/src/types/search.ts +0 -247
- package/src/types/security_rules.ts +0 -344
- package/src/types/storage_authorize.ts +0 -77
- package/src/types/storage_source.ts +0 -248
- package/src/types/websockets.ts +0 -117
- package/src/users/index.ts +0 -2
- package/src/users/user.ts +0 -69
package/dist/index.es.js
CHANGED
|
@@ -30,12 +30,18 @@ var RebaseApiError = class extends Error {
|
|
|
30
30
|
code;
|
|
31
31
|
/** Structured error payload from the server, when present. */
|
|
32
32
|
details;
|
|
33
|
+
/** See {@link RebaseErrorInit.requestId}. Quote it in a bug report. */
|
|
34
|
+
requestId;
|
|
35
|
+
/** See {@link RebaseErrorInit.retryAfterSeconds}. */
|
|
36
|
+
retryAfterSeconds;
|
|
33
37
|
constructor(message, init = {}) {
|
|
34
38
|
super(message);
|
|
35
39
|
this.name = "RebaseApiError";
|
|
36
40
|
this.status = init.status;
|
|
37
41
|
this.code = init.code;
|
|
38
42
|
this.details = init.details;
|
|
43
|
+
this.requestId = init.requestId;
|
|
44
|
+
this.retryAfterSeconds = init.retryAfterSeconds;
|
|
39
45
|
if (init.cause !== void 0) this.cause = init.cause;
|
|
40
46
|
}
|
|
41
47
|
};
|
|
@@ -49,11 +55,76 @@ var RebaseApiError = class extends Error {
|
|
|
49
55
|
* @group Errors
|
|
50
56
|
*/
|
|
51
57
|
var RebaseClientError = class extends RebaseApiError {
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
/**
|
|
59
|
+
* `init` is the same one {@link RebaseApiError} takes, and it is what makes
|
|
60
|
+
* `code` reachable at all.
|
|
61
|
+
*
|
|
62
|
+
* The constructor used to accept a message and nothing else, so every
|
|
63
|
+
* client-side failure — an undefined filter value, an unknown accessor,
|
|
64
|
+
* `listen()` on a client built with `realtime: false`, a function name with
|
|
65
|
+
* a `/` in it, `refreshSession()` while signed out — arrived with `code ===
|
|
66
|
+
* undefined`. The documented `switch (e.code)` in this file's own example
|
|
67
|
+
* fell to `default: throw e` for all of them, and the only client-side error
|
|
68
|
+
* that *did* carry a code was `OFFLINE`, because that one path minted a
|
|
69
|
+
* `RebaseApiError` instead.
|
|
70
|
+
*/
|
|
71
|
+
constructor(message, init = {}) {
|
|
72
|
+
super(message, init);
|
|
54
73
|
this.name = "RebaseClientError";
|
|
55
74
|
}
|
|
56
75
|
};
|
|
76
|
+
/**
|
|
77
|
+
* Brand for a contract method a particular client cannot serve.
|
|
78
|
+
*
|
|
79
|
+
* `Symbol.for` rather than a fresh symbol: two copies of `@rebasepro/types` in
|
|
80
|
+
* one tree — which happens, see `docs/dependency-duplication-traps.md` — must
|
|
81
|
+
* agree about it, and a module-local symbol would not.
|
|
82
|
+
*/
|
|
83
|
+
var UNSUPPORTED_METHOD = Symbol.for("rebase.unsupportedMethod");
|
|
84
|
+
/**
|
|
85
|
+
* Build the stub a client installs for a contract method it cannot serve.
|
|
86
|
+
*
|
|
87
|
+
* `listen`, `listenById` and `count` are part of `SDKCollectionClient`, not
|
|
88
|
+
* optional extras — a caller should be able to write
|
|
89
|
+
* `client.data.posts.count()` without asking first, and a transport that cannot
|
|
90
|
+
* serve it should answer with a sentence naming the configuration that would,
|
|
91
|
+
* rather than with `undefined is not a function` at the call site. Where the
|
|
92
|
+
* transport genuinely cannot (a client built with `realtime: false`, a driver
|
|
93
|
+
* with no `listenCollection`), it installs one of these instead of omitting the
|
|
94
|
+
* method.
|
|
95
|
+
*
|
|
96
|
+
* @param message What to tell the caller, naming the fix.
|
|
97
|
+
* @group Errors
|
|
98
|
+
*/
|
|
99
|
+
function unsupportedMethod(message) {
|
|
100
|
+
const stub = () => {
|
|
101
|
+
throw new RebaseClientError(message, { code: "REALTIME_DISABLED" });
|
|
102
|
+
};
|
|
103
|
+
stub[UNSUPPORTED_METHOD] = true;
|
|
104
|
+
return stub;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Can this method actually do anything?
|
|
108
|
+
*
|
|
109
|
+
* `true` for a stub from {@link unsupportedMethod} **and** for a method that is
|
|
110
|
+
* simply not there — a partial client, a hand-built test double, an
|
|
111
|
+
* implementation written against an older shape of the interface. Both mean the
|
|
112
|
+
* same thing to a caller, so both answer the same way, and an adapter that
|
|
113
|
+
* checks this cannot be caught out by either.
|
|
114
|
+
*
|
|
115
|
+
* Ordinary code does not need it: calling the method and letting it throw is
|
|
116
|
+
* the normal path. Adapters do — the admin panel chooses between subscribing
|
|
117
|
+
* and a one-shot `find()` by asking whether the client can listen, and a UI
|
|
118
|
+
* that subscribes into a throw is worse than one that polls. This is the
|
|
119
|
+
* question `if (accessor.listen)` used to be asking, made explicit now that the
|
|
120
|
+
* method is always there to call.
|
|
121
|
+
*
|
|
122
|
+
* @group Errors
|
|
123
|
+
*/
|
|
124
|
+
function isUnsupported(method) {
|
|
125
|
+
if (typeof method !== "function") return true;
|
|
126
|
+
return method[UNSUPPORTED_METHOD] === true;
|
|
127
|
+
}
|
|
57
128
|
//#endregion
|
|
58
129
|
//#region src/types/entities.ts
|
|
59
130
|
/**
|
|
@@ -363,6 +434,7 @@ var ADMIN_COLLECTION_KEYS = [
|
|
|
363
434
|
"Actions",
|
|
364
435
|
"additionalFields",
|
|
365
436
|
"alwaysApplyDefaultValues",
|
|
437
|
+
"browserCallbacks",
|
|
366
438
|
"components",
|
|
367
439
|
"customViews",
|
|
368
440
|
"defaultEntityAction",
|
|
@@ -430,6 +502,7 @@ var ADMIN_PROPERTY_KEYS = [
|
|
|
430
502
|
"Filter",
|
|
431
503
|
"filterOperators",
|
|
432
504
|
"fixedFilter",
|
|
505
|
+
"format",
|
|
433
506
|
"hideFromCollection",
|
|
434
507
|
"includeEntityLink",
|
|
435
508
|
"includeId",
|
|
@@ -440,6 +513,7 @@ var ADMIN_PROPERTY_KEYS = [
|
|
|
440
513
|
"previewAsTag",
|
|
441
514
|
"previewProperties",
|
|
442
515
|
"readOnly",
|
|
516
|
+
"renderInForm",
|
|
443
517
|
"sortable",
|
|
444
518
|
"span",
|
|
445
519
|
"spreadChildren",
|
|
@@ -821,6 +895,7 @@ var policy = {
|
|
|
821
895
|
roles
|
|
822
896
|
}),
|
|
823
897
|
authenticated: () => ({ kind: "authenticated" }),
|
|
898
|
+
registered: () => ({ kind: "registered" }),
|
|
824
899
|
serverContext: () => ({ kind: "serverContext" }),
|
|
825
900
|
existsIn: (args) => ({
|
|
826
901
|
kind: "existsIn",
|
|
@@ -906,6 +981,17 @@ var REBASE_SCHEMA = "rebase";
|
|
|
906
981
|
var RLS_UID_SQL = `${REBASE_SCHEMA}.uid()`;
|
|
907
982
|
/** The request's roles as a comma-separated string, for `string_to_array`. */
|
|
908
983
|
var RLS_ROLES_SQL = `${REBASE_SCHEMA}.roles()`;
|
|
984
|
+
/**
|
|
985
|
+
* Whether the caller is a GUEST — signed in through anonymous sign-in rather
|
|
986
|
+
* than with an account.
|
|
987
|
+
*
|
|
988
|
+
* A different question from {@link ANONYMOUS_USER_ID}, and the two are easy to
|
|
989
|
+
* confuse: that sentinel means "no session at all", while this means "a session
|
|
990
|
+
* with nobody behind it". Anonymous sign-in mints a real user row with a real
|
|
991
|
+
* uid, so before this reached the database the two kinds of caller were one
|
|
992
|
+
* principal inside every policy.
|
|
993
|
+
*/
|
|
994
|
+
var RLS_IS_ANONYMOUS_SQL = `${REBASE_SCHEMA}.is_anonymous()`;
|
|
909
995
|
/** The request's JWT claims as `jsonb`, or `{}`. */
|
|
910
996
|
var RLS_JWT_SQL = `${REBASE_SCHEMA}.jwt()`;
|
|
911
997
|
/**
|
|
@@ -1015,36 +1101,170 @@ function isResourceHandle(value) {
|
|
|
1015
1101
|
function resourceKeyOf(ref) {
|
|
1016
1102
|
return isResourceHandle(ref) ? ref.key : ref;
|
|
1017
1103
|
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Replace every resource handle inside a value with its key, deeply.
|
|
1106
|
+
*
|
|
1107
|
+
* Applied where authored config becomes data: `defineCollection`, the
|
|
1108
|
+
* collection loaders, the derive step. Past that point a collection is plain
|
|
1109
|
+
* data that serialises, compares with `===` and reaches the admin UI over the
|
|
1110
|
+
* wire, so a handle must not survive into it. Plain objects and arrays are
|
|
1111
|
+
* walked; anything else — a function, a Date, a class instance — is a leaf and
|
|
1112
|
+
* is returned as it is, which is what keeps callbacks and validators intact.
|
|
1113
|
+
*/
|
|
1114
|
+
function resolveResourceRefs(value) {
|
|
1115
|
+
if (isResourceHandle(value)) return value.key;
|
|
1116
|
+
if (Array.isArray(value)) {
|
|
1117
|
+
let changed = false;
|
|
1118
|
+
const out = value.map((item) => {
|
|
1119
|
+
const next = resolveResourceRefs(item);
|
|
1120
|
+
if (next !== item) changed = true;
|
|
1121
|
+
return next;
|
|
1122
|
+
});
|
|
1123
|
+
return changed ? out : value;
|
|
1124
|
+
}
|
|
1125
|
+
if (value !== null && typeof value === "object") {
|
|
1126
|
+
const proto = Object.getPrototypeOf(value);
|
|
1127
|
+
if (proto === Object.prototype || proto === null) {
|
|
1128
|
+
let changed = false;
|
|
1129
|
+
const out = {};
|
|
1130
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1131
|
+
const next = resolveResourceRefs(v);
|
|
1132
|
+
if (next !== v) changed = true;
|
|
1133
|
+
out[k] = next;
|
|
1134
|
+
}
|
|
1135
|
+
return changed ? out : value;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
return value;
|
|
1139
|
+
}
|
|
1018
1140
|
var GLOBAL_KEY = Symbol.for("@rebasepro/types.resourceRegistry");
|
|
1141
|
+
/**
|
|
1142
|
+
* Where kinds live, versioned — and why the version is in the symbol.
|
|
1143
|
+
*
|
|
1144
|
+
* Sharing one kinds map across copies means the copy that registers SECOND is
|
|
1145
|
+
* the one that runs the comparison. That copy is whatever the bundle happens to
|
|
1146
|
+
* carry, which for a driver is a build of this package frozen at its release —
|
|
1147
|
+
* so the rule enforced is the rule that shipped THEN, not the one written here.
|
|
1148
|
+
*
|
|
1149
|
+
* `revision` (390bb03cd, applied to `database` in 346df48e2) was supposed to
|
|
1150
|
+
* settle a disagreement between two copies, and it settles it only when the
|
|
1151
|
+
* copy doing the arithmetic knows what `revision` is. 0.17.0–0.17.3 do not:
|
|
1152
|
+
* they deep-equal the spec and throw. The runtime registers at import and a
|
|
1153
|
+
* driver is imported after it, so the old copy is always second, always the
|
|
1154
|
+
* judge, and always throws — verified by the bundle corpus on 2026-09-07,
|
|
1155
|
+
* which reported v0.17.3's message verbatim ("Two packages cannot define the
|
|
1156
|
+
* same kind.", no revision clause) while the runtime it ran on was 0.18.
|
|
1157
|
+
*
|
|
1158
|
+
* So copies that understand `revision` keep their kinds here, under a symbol
|
|
1159
|
+
* no released copy looks at, and the legacy map is left to whoever still wants
|
|
1160
|
+
* it. An old copy then registers into a map nobody contests, finds no existing
|
|
1161
|
+
* entry, and cannot throw — in any load order, which is what the previous fix
|
|
1162
|
+
* only claimed. Bumping this suffix again is how a future change to the
|
|
1163
|
+
* REGISTRATION PROTOCOL is made; a change to a kind's own definition is still
|
|
1164
|
+
* `revision`, among peers that share this map.
|
|
1165
|
+
*/
|
|
1166
|
+
var KINDS_KEY = Symbol.for("@rebasepro/types.resourceKinds.v2");
|
|
1019
1167
|
function registry() {
|
|
1020
1168
|
const g = globalThis;
|
|
1021
|
-
let
|
|
1022
|
-
if (!
|
|
1023
|
-
|
|
1169
|
+
let shared = g[GLOBAL_KEY];
|
|
1170
|
+
if (!shared) {
|
|
1171
|
+
shared = {
|
|
1024
1172
|
kinds: /* @__PURE__ */ new Map(),
|
|
1025
1173
|
declarations: /* @__PURE__ */ new Map()
|
|
1026
1174
|
};
|
|
1027
|
-
g[GLOBAL_KEY] =
|
|
1175
|
+
g[GLOBAL_KEY] = shared;
|
|
1028
1176
|
}
|
|
1029
|
-
|
|
1177
|
+
let kinds = g[KINDS_KEY];
|
|
1178
|
+
if (!kinds) {
|
|
1179
|
+
kinds = /* @__PURE__ */ new Map();
|
|
1180
|
+
g[KINDS_KEY] = kinds;
|
|
1181
|
+
}
|
|
1182
|
+
return {
|
|
1183
|
+
kinds,
|
|
1184
|
+
legacyKinds: shared.kinds,
|
|
1185
|
+
declarations: shared.declarations
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* Every kind visible to this copy: the versioned map, plus anything only a
|
|
1190
|
+
* legacy copy registered.
|
|
1191
|
+
*
|
|
1192
|
+
* The fallback is not for Rebase's own kinds — this copy defines all of those —
|
|
1193
|
+
* but for a third-party driver built against an older `@rebasepro/types` that
|
|
1194
|
+
* registers a kind of its own. Dropping it would make that kind invisible and
|
|
1195
|
+
* turn `declareResource` into "unknown resource kind" for something genuinely
|
|
1196
|
+
* registered.
|
|
1197
|
+
*/
|
|
1198
|
+
function visibleKinds() {
|
|
1199
|
+
const { kinds, legacyKinds } = registry();
|
|
1200
|
+
if (legacyKinds.size === 0) return kinds;
|
|
1201
|
+
const merged = new Map(legacyKinds);
|
|
1202
|
+
for (const [k, v] of kinds) merged.set(k, v);
|
|
1203
|
+
return merged;
|
|
1204
|
+
}
|
|
1205
|
+
var amendments = /* @__PURE__ */ new Map();
|
|
1206
|
+
/**
|
|
1207
|
+
* Correct a registered kind without touching its registered literal.
|
|
1208
|
+
*
|
|
1209
|
+
* Use this, never an edit to the literal, for a kind that has shipped in a
|
|
1210
|
+
* published package. The amendment applies to reads through this copy only.
|
|
1211
|
+
*/
|
|
1212
|
+
function amendResourceKind(kind, amendment) {
|
|
1213
|
+
amendments.set(kind, {
|
|
1214
|
+
...amendments.get(kind),
|
|
1215
|
+
...amendment
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
/** A registered kind as this copy sees it: the shared literal plus this copy's amendments. */
|
|
1219
|
+
function effectiveKind(spec) {
|
|
1220
|
+
const amendment = amendments.get(spec.kind);
|
|
1221
|
+
return amendment ? {
|
|
1222
|
+
...spec,
|
|
1223
|
+
...amendment
|
|
1224
|
+
} : spec;
|
|
1030
1225
|
}
|
|
1031
1226
|
/** `kind:key`, the graph's primary key. */
|
|
1032
1227
|
function declarationId(kind, key) {
|
|
1033
1228
|
return `${kind}:${key}`;
|
|
1034
1229
|
}
|
|
1035
|
-
/**
|
|
1230
|
+
/**
|
|
1231
|
+
* Register a resource kind.
|
|
1232
|
+
*
|
|
1233
|
+
* Idempotent for an identical spec. When a spec for the same kind is already
|
|
1234
|
+
* registered and differs, the `revision` decides: the higher one is kept and
|
|
1235
|
+
* the other copy is warned about, in either load order. Two different specs at
|
|
1236
|
+
* the SAME revision are a genuine conflict — two packages defining one kind, or
|
|
1237
|
+
* a change that forgot to bump — and still throw.
|
|
1238
|
+
*
|
|
1239
|
+
* Both copies in that comparison are peers on `KINDS_KEY`, which is what makes
|
|
1240
|
+
* the rule enforceable: a copy old enough not to know `revision` writes to the
|
|
1241
|
+
* legacy map instead and never reaches this function's arithmetic. Registering
|
|
1242
|
+
* a kind an older copy already put in the legacy map is therefore not a
|
|
1243
|
+
* conflict — it is the ordinary case, and `visibleKinds` prefers this one.
|
|
1244
|
+
*/
|
|
1036
1245
|
function registerResourceKind(spec) {
|
|
1037
|
-
const
|
|
1038
|
-
|
|
1039
|
-
|
|
1246
|
+
const kinds = registry().kinds;
|
|
1247
|
+
const existing = kinds.get(spec.kind);
|
|
1248
|
+
if (!existing) {
|
|
1249
|
+
kinds.set(spec.kind, spec);
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
if (JSON.stringify(existing) === JSON.stringify(spec)) return;
|
|
1253
|
+
const have = existing.revision ?? 0;
|
|
1254
|
+
const incoming = spec.revision ?? 0;
|
|
1255
|
+
if (have === incoming) throw new Error(`Resource kind "${spec.kind}" is already registered with a different definition at revision ${have}. Two packages cannot define the same kind; a newer definition of the same kind must carry a higher \`revision\`.`);
|
|
1256
|
+
const [kept, dropped] = incoming > have ? [spec, existing] : [existing, spec];
|
|
1257
|
+
if (kept === spec) kinds.set(spec.kind, spec);
|
|
1258
|
+
console.warn(`[resources] Resource kind "${spec.kind}" is registered twice, at revisions ${dropped.revision ?? 0} and ${kept.revision ?? 0}; keeping revision ${kept.revision ?? 0}. The older copy is usually @rebasepro/types inlined in a driver built before the kind changed — rebuild the project with a current driver to remove it.`);
|
|
1040
1259
|
}
|
|
1041
1260
|
/** Every registered kind, for validators and for `rebase doctor`. */
|
|
1042
1261
|
function resourceKinds() {
|
|
1043
|
-
return [...
|
|
1262
|
+
return [...visibleKinds().values()].map(effectiveKind);
|
|
1044
1263
|
}
|
|
1045
1264
|
/** One registered kind, or undefined. */
|
|
1046
1265
|
function resourceKind(kind) {
|
|
1047
|
-
|
|
1266
|
+
const spec = visibleKinds().get(kind);
|
|
1267
|
+
return spec && effectiveKind(spec);
|
|
1048
1268
|
}
|
|
1049
1269
|
var COMMON_OPTION_KEYS = [
|
|
1050
1270
|
"engine",
|
|
@@ -1065,9 +1285,9 @@ function isValidEngine(spec, engine) {
|
|
|
1065
1285
|
* fine: a config module evaluated twice must not be an error.
|
|
1066
1286
|
*/
|
|
1067
1287
|
function declareResource(kind, key = DEFAULT_RESOURCE_KEY, options = {}) {
|
|
1068
|
-
const spec =
|
|
1288
|
+
const spec = resourceKind(kind);
|
|
1069
1289
|
if (!spec) {
|
|
1070
|
-
const known = [...
|
|
1290
|
+
const known = [...visibleKinds().keys()].sort().join(", ") || "none";
|
|
1071
1291
|
throw new Error(`Unknown resource kind "${kind}". Registered kinds: ${known}. Call registerResourceKind() before declaring one.`);
|
|
1072
1292
|
}
|
|
1073
1293
|
if (!key || typeof key !== "string" || key.trim() === "") throw new Error(`A ${kind} needs a non-empty key.`);
|
|
@@ -1149,13 +1369,29 @@ function findEnvSuffixCollision(keys) {
|
|
|
1149
1369
|
}
|
|
1150
1370
|
/** The current graph format version. */
|
|
1151
1371
|
var RESOURCE_GRAPH_VERSION = 1;
|
|
1152
|
-
/**
|
|
1153
|
-
|
|
1372
|
+
/**
|
|
1373
|
+
* Build a graph from the current declarations, sorted for a stable diff.
|
|
1374
|
+
*
|
|
1375
|
+
* `usedBy` maps a `kind:key` id to the things that reach it. The derive step
|
|
1376
|
+
* supplies it after evaluating collections; a runtime building the graph at
|
|
1377
|
+
* boot passes nothing and gets declarations alone, which is all it binds from.
|
|
1378
|
+
*/
|
|
1379
|
+
function buildResourceGraph(usedBy) {
|
|
1154
1380
|
return {
|
|
1155
1381
|
version: 1,
|
|
1156
|
-
resources: declaredResources().slice().sort((a, b) => a.kind.localeCompare(b.kind) || a.key.localeCompare(b.key))
|
|
1382
|
+
resources: declaredResources().slice().sort((a, b) => a.kind.localeCompare(b.kind) || a.key.localeCompare(b.key)).map((r) => {
|
|
1383
|
+
const users = usedBy?.get(declarationId(r.kind, r.key));
|
|
1384
|
+
return users && users.length > 0 ? {
|
|
1385
|
+
...r,
|
|
1386
|
+
usedBy: [...users].sort()
|
|
1387
|
+
} : r;
|
|
1388
|
+
})
|
|
1157
1389
|
};
|
|
1158
1390
|
}
|
|
1391
|
+
/** `kind:key`, the id `usedBy` maps are keyed by. Exported for the derive step. */
|
|
1392
|
+
function resourceId(kind, key) {
|
|
1393
|
+
return declarationId(kind, key);
|
|
1394
|
+
}
|
|
1159
1395
|
/**
|
|
1160
1396
|
* The environment variables worth writing for a resource, given its engine.
|
|
1161
1397
|
*
|
|
@@ -1169,6 +1405,78 @@ function envBasesForResource(declaration) {
|
|
|
1169
1405
|
return spec.envBasesByEngine?.[declaration.engine] ?? spec.envBases;
|
|
1170
1406
|
}
|
|
1171
1407
|
//#endregion
|
|
1408
|
+
//#region src/types/storage_source.ts
|
|
1409
|
+
/**
|
|
1410
|
+
* Describes a named storage backend — a place files live.
|
|
1411
|
+
*
|
|
1412
|
+
* Declared once and shared front + back: the frontend uses it to decide
|
|
1413
|
+
* transport (HTTP proxy vs direct SDK), the backend uses the same `key`
|
|
1414
|
+
* to resolve a StorageController, and collection properties reference
|
|
1415
|
+
* a definition by its `key` via `StorageConfig.storageSource`.
|
|
1416
|
+
*
|
|
1417
|
+
* This mirrors the {@link DataSourceDefinition} pattern used for databases.
|
|
1418
|
+
*
|
|
1419
|
+
* @group Models
|
|
1420
|
+
*/
|
|
1421
|
+
/**
|
|
1422
|
+
* The default storage source key, used when a property does not specify
|
|
1423
|
+
* a `storageSource`. Shared by the frontend and backend registries so
|
|
1424
|
+
* both agree on "the default storage backend".
|
|
1425
|
+
* @group Models
|
|
1426
|
+
*/
|
|
1427
|
+
var DEFAULT_STORAGE_SOURCE_KEY = "(default)";
|
|
1428
|
+
/**
|
|
1429
|
+
* The environment-variable suffix for a storage or data source key.
|
|
1430
|
+
*
|
|
1431
|
+
* `""` for the default source — so a single-bucket project keeps configuring
|
|
1432
|
+
* plain `S3_BUCKET` — and `__<KEY>` for every named one, uppercased with
|
|
1433
|
+
* non-alphanumerics collapsed to underscores: `media-cdn` → `S3_BUCKET__MEDIA_CDN`.
|
|
1434
|
+
*
|
|
1435
|
+
* The rule derives the variable name from the declared key rather than
|
|
1436
|
+
* discovering keys by scanning the environment. Scanning would have to guess how
|
|
1437
|
+
* `S3_BUCKET__MEDIA_CDN` splits into a key; deriving cannot be ambiguous, and a
|
|
1438
|
+
* typo surfaces as a missing source at boot instead of a silently ignored
|
|
1439
|
+
* variable.
|
|
1440
|
+
*
|
|
1441
|
+
* It lives in this package, with no dependencies, because four things must agree
|
|
1442
|
+
* on it exactly: the CLI (validating a build), the runtime (reading its own
|
|
1443
|
+
* environment), the control plane (writing a tenant's Secret), and the docs. A
|
|
1444
|
+
* second implementation of a naming convention is a second chance to disagree.
|
|
1445
|
+
*
|
|
1446
|
+
* @group Models
|
|
1447
|
+
*/
|
|
1448
|
+
function storageEnvSuffix(key, defaultKey = DEFAULT_STORAGE_SOURCE_KEY) {
|
|
1449
|
+
if (!key || key === defaultKey) return "";
|
|
1450
|
+
const normalized = key.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
|
|
1451
|
+
if (!normalized) throw new Error(`Source key "${key}" cannot be turned into an environment variable name. Use a key containing at least one letter or digit.`);
|
|
1452
|
+
return `__${normalized}`;
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Two distinct keys that collapse onto the same variable name, or `null`.
|
|
1456
|
+
*
|
|
1457
|
+
* `media-cdn` and `media_cdn` are different source keys but the same suffix, so
|
|
1458
|
+
* without this one of them silently reads the other's configuration. Returns the
|
|
1459
|
+
* offending pair rather than throwing, so each caller can raise it in its own
|
|
1460
|
+
* idiom — a `BundleError` at boot, a build failure in the CLI, a rejected deploy
|
|
1461
|
+
* in a control plane.
|
|
1462
|
+
*
|
|
1463
|
+
* @group Models
|
|
1464
|
+
*/
|
|
1465
|
+
function findStorageSuffixCollision(keys, defaultKey = DEFAULT_STORAGE_SOURCE_KEY) {
|
|
1466
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1467
|
+
for (const key of keys) {
|
|
1468
|
+
const suffix = storageEnvSuffix(key, defaultKey);
|
|
1469
|
+
const existing = seen.get(suffix);
|
|
1470
|
+
if (existing !== void 0 && existing !== key) return {
|
|
1471
|
+
a: existing,
|
|
1472
|
+
b: key,
|
|
1473
|
+
suffix
|
|
1474
|
+
};
|
|
1475
|
+
seen.set(suffix, key);
|
|
1476
|
+
}
|
|
1477
|
+
return null;
|
|
1478
|
+
}
|
|
1479
|
+
//#endregion
|
|
1172
1480
|
//#region src/types/resource_kinds.ts
|
|
1173
1481
|
/**
|
|
1174
1482
|
* The kinds Rebase ships, and the constructors a project declares them with.
|
|
@@ -1184,6 +1492,7 @@ function envBasesForResource(declaration) {
|
|
|
1184
1492
|
* instead of looking like a typo of one that exists.
|
|
1185
1493
|
*/
|
|
1186
1494
|
registerResourceKind({
|
|
1495
|
+
revision: 1,
|
|
1187
1496
|
kind: "database",
|
|
1188
1497
|
engines: [
|
|
1189
1498
|
"postgres",
|
|
@@ -1204,6 +1513,15 @@ registerResourceKind({
|
|
|
1204
1513
|
],
|
|
1205
1514
|
implicitDefault: true
|
|
1206
1515
|
});
|
|
1516
|
+
amendResourceKind("database", { envBases: [
|
|
1517
|
+
"DATABASE_URL",
|
|
1518
|
+
"DATABASE_READ_URL",
|
|
1519
|
+
"ADMIN_CONNECTION_STRING",
|
|
1520
|
+
"REBASE_DRIVER",
|
|
1521
|
+
"DB_POOL_MAX",
|
|
1522
|
+
"DB_POOL_IDLE_TIMEOUT",
|
|
1523
|
+
"DB_POOL_CONNECT_TIMEOUT"
|
|
1524
|
+
] });
|
|
1207
1525
|
function database(keyOrOptions = DEFAULT_RESOURCE_KEY, options = {}) {
|
|
1208
1526
|
return typeof keyOrOptions === "string" ? declareResource("database", keyOrOptions, options) : declareResource("database", DEFAULT_RESOURCE_KEY, keyOrOptions);
|
|
1209
1527
|
}
|
|
@@ -1263,18 +1581,49 @@ registerResourceKind({
|
|
|
1263
1581
|
],
|
|
1264
1582
|
implicitDefault: false
|
|
1265
1583
|
});
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1584
|
+
amendResourceKind("bucket", {
|
|
1585
|
+
optionKeys: [
|
|
1586
|
+
"publicRead",
|
|
1587
|
+
"prefix",
|
|
1588
|
+
"account",
|
|
1589
|
+
"default"
|
|
1590
|
+
],
|
|
1591
|
+
envBases: [
|
|
1592
|
+
"STORAGE_TYPE",
|
|
1593
|
+
"STORAGE_PATH",
|
|
1594
|
+
"S3_BUCKET",
|
|
1595
|
+
"S3_REGION",
|
|
1596
|
+
"S3_ACCESS_KEY_ID",
|
|
1597
|
+
"S3_SECRET_ACCESS_KEY",
|
|
1598
|
+
"S3_ENDPOINT",
|
|
1599
|
+
"S3_FORCE_PATH_STYLE",
|
|
1600
|
+
"GCS_BUCKET",
|
|
1601
|
+
"GCS_PROJECT_ID",
|
|
1602
|
+
"GCS_KEY_FILENAME"
|
|
1603
|
+
],
|
|
1604
|
+
envBasesByEngine: {
|
|
1605
|
+
local: ["STORAGE_TYPE", "STORAGE_PATH"],
|
|
1606
|
+
s3: [
|
|
1607
|
+
"STORAGE_TYPE",
|
|
1608
|
+
"S3_BUCKET",
|
|
1609
|
+
"S3_REGION",
|
|
1610
|
+
"S3_ACCESS_KEY_ID",
|
|
1611
|
+
"S3_SECRET_ACCESS_KEY",
|
|
1612
|
+
"S3_ENDPOINT",
|
|
1613
|
+
"S3_FORCE_PATH_STYLE"
|
|
1614
|
+
],
|
|
1615
|
+
gcs: [
|
|
1616
|
+
"STORAGE_TYPE",
|
|
1617
|
+
"GCS_BUCKET",
|
|
1618
|
+
"GCS_PROJECT_ID",
|
|
1619
|
+
"GCS_KEY_FILENAME"
|
|
1620
|
+
],
|
|
1621
|
+
azure: [],
|
|
1622
|
+
firebase: []
|
|
1623
|
+
}
|
|
1624
|
+
});
|
|
1625
|
+
function bucket(keyOrOptions = DEFAULT_RESOURCE_KEY, options = {}) {
|
|
1626
|
+
return typeof keyOrOptions === "string" ? declareResource("bucket", keyOrOptions, options) : declareResource("bucket", DEFAULT_RESOURCE_KEY, keyOrOptions);
|
|
1278
1627
|
}
|
|
1279
1628
|
registerResourceKind({
|
|
1280
1629
|
kind: "topic",
|
|
@@ -1284,6 +1633,7 @@ registerResourceKind({
|
|
|
1284
1633
|
optionKeys: ["delivery", "maxAttempts"],
|
|
1285
1634
|
implicitDefault: false
|
|
1286
1635
|
});
|
|
1636
|
+
amendResourceKind("topic", { envBases: [] });
|
|
1287
1637
|
var runtimeHolder = { current: null };
|
|
1288
1638
|
/** Install the transport topics publish through. Called by the server at boot. */
|
|
1289
1639
|
function setTopicRuntime(runtime) {
|
|
@@ -1331,10 +1681,145 @@ function topic(key, options = {}) {
|
|
|
1331
1681
|
}
|
|
1332
1682
|
};
|
|
1333
1683
|
}
|
|
1684
|
+
registerResourceKind({
|
|
1685
|
+
kind: "cron",
|
|
1686
|
+
engines: ["scheduler"],
|
|
1687
|
+
defaultEngine: "scheduler",
|
|
1688
|
+
envBases: [],
|
|
1689
|
+
optionKeys: [
|
|
1690
|
+
"schedule",
|
|
1691
|
+
"timezone",
|
|
1692
|
+
"description",
|
|
1693
|
+
"enabled",
|
|
1694
|
+
"timeoutSeconds",
|
|
1695
|
+
"catchUpWindowSeconds"
|
|
1696
|
+
],
|
|
1697
|
+
implicitDefault: false
|
|
1698
|
+
});
|
|
1699
|
+
/**
|
|
1700
|
+
* Declare a cron, as the scheduler's `defineCron` does on its way through.
|
|
1701
|
+
*
|
|
1702
|
+
* Projects do not call this: `defineCron` in `@rebasepro/server` does, so a
|
|
1703
|
+
* cron file is both the handler and the declaration — one file, one name, and
|
|
1704
|
+
* the graph derived from it says what a host needs to know without evaluating
|
|
1705
|
+
* the handler. Exported so the derive step and the scheduler spell the
|
|
1706
|
+
* declaration identically.
|
|
1707
|
+
*/
|
|
1708
|
+
function declareCron(name, options) {
|
|
1709
|
+
if (typeof options.schedule !== "string" || options.schedule.trim() === "") throw new Error(`Cron "${name}" needs a schedule — a five-field cron expression such as "0 3 * * *".`);
|
|
1710
|
+
return declareResource("cron", name, options);
|
|
1711
|
+
}
|
|
1712
|
+
registerResourceKind({
|
|
1713
|
+
kind: "function",
|
|
1714
|
+
engines: ["http"],
|
|
1715
|
+
defaultEngine: "http",
|
|
1716
|
+
envBases: [],
|
|
1717
|
+
optionKeys: [
|
|
1718
|
+
"portable",
|
|
1719
|
+
"requires",
|
|
1720
|
+
"file"
|
|
1721
|
+
],
|
|
1722
|
+
implicitDefault: false
|
|
1723
|
+
});
|
|
1724
|
+
/** Declare a function. Called by the derive step, not by projects. */
|
|
1725
|
+
function declareFunction(name, options = {}) {
|
|
1726
|
+
return declareResource("function", name, options);
|
|
1727
|
+
}
|
|
1728
|
+
registerResourceKind({
|
|
1729
|
+
kind: "queue",
|
|
1730
|
+
engines: ["jobs"],
|
|
1731
|
+
defaultEngine: "jobs",
|
|
1732
|
+
envBases: [],
|
|
1733
|
+
optionKeys: ["maxAttempts"],
|
|
1734
|
+
implicitDefault: false
|
|
1735
|
+
});
|
|
1736
|
+
var queueRuntimeHolder = { current: null };
|
|
1737
|
+
/** Install the transport queues enqueue through. Called by the server at boot. */
|
|
1738
|
+
function setQueueRuntime(runtime) {
|
|
1739
|
+
queueRuntimeHolder.current = runtime;
|
|
1740
|
+
}
|
|
1741
|
+
var queueConsumers = /* @__PURE__ */ new Map();
|
|
1742
|
+
/** Every declared queue handler, for the worker to wire. */
|
|
1743
|
+
function declaredQueueConsumers() {
|
|
1744
|
+
return [...queueConsumers.values()];
|
|
1745
|
+
}
|
|
1746
|
+
/** Forget declared queue handlers. For tests, alongside `resetDeclaredResources`. */
|
|
1747
|
+
function resetDeclaredQueueConsumers() {
|
|
1748
|
+
queueConsumers.clear();
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* Declare a queue.
|
|
1752
|
+
*
|
|
1753
|
+
* ```ts
|
|
1754
|
+
* export const thumbnails = queue<{ key: string }>("thumbnails");
|
|
1755
|
+
* thumbnails.handler(async ({ key }) => { … });
|
|
1756
|
+
* await thumbnails.enqueue({ key }, { runAt: new Date(Date.now() + 60_000) });
|
|
1757
|
+
* ```
|
|
1758
|
+
*
|
|
1759
|
+
* The difference from a topic is the number of consumers: a queue has one, a
|
|
1760
|
+
* topic fans out to every subscription. Both ride on the durable job queue, so
|
|
1761
|
+
* declaring either turns it on.
|
|
1762
|
+
*/
|
|
1763
|
+
function queue(key, options = {}) {
|
|
1764
|
+
return {
|
|
1765
|
+
...declareResource("queue", key, options),
|
|
1766
|
+
toString() {
|
|
1767
|
+
return key;
|
|
1768
|
+
},
|
|
1769
|
+
async enqueue(payload, enqueueOptions) {
|
|
1770
|
+
const runtime = queueRuntimeHolder.current;
|
|
1771
|
+
if (!runtime) throw new Error(`Cannot enqueue on queue "${key}": no queue runtime is installed. Enqueueing works inside a running Rebase backend; this looks like config being evaluated outside one (a build, a script, or a test without a harness).`);
|
|
1772
|
+
return runtime.enqueue(key, payload, enqueueOptions);
|
|
1773
|
+
},
|
|
1774
|
+
handler(fn) {
|
|
1775
|
+
if (queueConsumers.has(key)) throw new Error(`Queue "${key}" already has a handler. A queue has exactly one consumer; work that several things react to is a topic with several subscriptions.`);
|
|
1776
|
+
queueConsumers.set(key, {
|
|
1777
|
+
queue: key,
|
|
1778
|
+
handler: fn
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* One declaration, as the data layer's definition.
|
|
1785
|
+
*
|
|
1786
|
+
* There is exactly one of these per kind, and everything that needs a
|
|
1787
|
+
* definition goes through it — the frontend, the managed runtime's boot path,
|
|
1788
|
+
* and an ejected project's own entrypoint. That is not tidiness: the mapping
|
|
1789
|
+
* used to exist twice, once here and once in `@rebasepro/server`'s
|
|
1790
|
+
* `graphToStorageSources`, and the two disagreed. The server's copy carried a
|
|
1791
|
+
* bucket's `account`; this one dropped it, so a bucket declared with shared
|
|
1792
|
+
* credentials resolved them on the managed runtime and resolved *nothing* in an
|
|
1793
|
+
* ejected backend — the source was skipped and every upload to it answered 501.
|
|
1794
|
+
*
|
|
1795
|
+
* A field-by-field map is one line away from that failure at all times, so
|
|
1796
|
+
* there is now one line to keep right instead of two to keep equal.
|
|
1797
|
+
*/
|
|
1798
|
+
function resourceToDataSource(declaration) {
|
|
1799
|
+
return {
|
|
1800
|
+
key: declaration.key === "(default)" ? DEFAULT_DATA_SOURCE_KEY : declaration.key,
|
|
1801
|
+
engine: declaration.engine,
|
|
1802
|
+
transport: declaration.transport,
|
|
1803
|
+
...typeof declaration.options.databaseId === "string" ? { databaseId: declaration.options.databaseId } : {},
|
|
1804
|
+
...declaration.label !== void 0 ? { label: declaration.label } : {}
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
/** One declaration, as the storage layer's definition. See {@link resourceToDataSource}. */
|
|
1808
|
+
function resourceToStorageSource(declaration) {
|
|
1809
|
+
return {
|
|
1810
|
+
key: declaration.key === "(default)" ? DEFAULT_STORAGE_SOURCE_KEY : declaration.key,
|
|
1811
|
+
engine: declaration.engine,
|
|
1812
|
+
transport: declaration.transport,
|
|
1813
|
+
...typeof declaration.options.account === "string" ? { account: declaration.options.account } : {},
|
|
1814
|
+
...declaration.options.default === true ? { default: true } : {},
|
|
1815
|
+
...declaration.label !== void 0 ? { label: declaration.label } : {}
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1334
1818
|
/**
|
|
1335
|
-
* The declared databases,
|
|
1819
|
+
* The declared databases, as definitions.
|
|
1336
1820
|
*
|
|
1337
|
-
*
|
|
1821
|
+
* Both the frontend and a project's own backend entrypoint read this. The
|
|
1822
|
+
* frontend needs to know which sources exist and how they are reached — a
|
|
1338
1823
|
* `direct`-transport source is one the browser talks to itself — and it imports
|
|
1339
1824
|
* the same config package the backend does. Without these it would mean writing
|
|
1340
1825
|
* the list a second time, by hand, next to the declarations, which is precisely
|
|
@@ -1352,134 +1837,11 @@ function topic(key, options = {}) {
|
|
|
1352
1837
|
* bare re-export.
|
|
1353
1838
|
*/
|
|
1354
1839
|
function declaredDataSources() {
|
|
1355
|
-
return declaredResources("database").map(
|
|
1356
|
-
key: r.key,
|
|
1357
|
-
engine: r.engine,
|
|
1358
|
-
transport: r.transport,
|
|
1359
|
-
...r.label !== void 0 ? { label: r.label } : {}
|
|
1360
|
-
}));
|
|
1361
|
-
}
|
|
1362
|
-
/** The declared buckets, in the shape `<Rebase storageSources>` takes. */
|
|
1363
|
-
function declaredStorageSources() {
|
|
1364
|
-
return declaredResources("bucket").map((r) => ({
|
|
1365
|
-
key: r.key,
|
|
1366
|
-
engine: r.engine,
|
|
1367
|
-
transport: r.transport,
|
|
1368
|
-
...r.label !== void 0 ? { label: r.label } : {}
|
|
1369
|
-
}));
|
|
1840
|
+
return declaredResources("database").map(resourceToDataSource);
|
|
1370
1841
|
}
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
* Describes a named storage backend — a place files live.
|
|
1375
|
-
*
|
|
1376
|
-
* Declared once and shared front + back: the frontend uses it to decide
|
|
1377
|
-
* transport (HTTP proxy vs direct SDK), the backend uses the same `key`
|
|
1378
|
-
* to resolve a StorageController, and collection properties reference
|
|
1379
|
-
* a definition by its `key` via `StorageConfig.storageSource`.
|
|
1380
|
-
*
|
|
1381
|
-
* This mirrors the {@link DataSourceDefinition} pattern used for databases.
|
|
1382
|
-
*
|
|
1383
|
-
* @group Models
|
|
1384
|
-
*/
|
|
1385
|
-
/**
|
|
1386
|
-
* The default storage source key, used when a property does not specify
|
|
1387
|
-
* a `storageSource`. Shared by the frontend and backend registries so
|
|
1388
|
-
* both agree on "the default storage backend".
|
|
1389
|
-
* @group Models
|
|
1390
|
-
*/
|
|
1391
|
-
var DEFAULT_STORAGE_SOURCE_KEY = "(default)";
|
|
1392
|
-
/**
|
|
1393
|
-
* The environment-variable suffix for a storage or data source key.
|
|
1394
|
-
*
|
|
1395
|
-
* `""` for the default source — so a single-bucket project keeps configuring
|
|
1396
|
-
* plain `S3_BUCKET` — and `__<KEY>` for every named one, uppercased with
|
|
1397
|
-
* non-alphanumerics collapsed to underscores: `media-cdn` → `S3_BUCKET__MEDIA_CDN`.
|
|
1398
|
-
*
|
|
1399
|
-
* The rule derives the variable name from the declared key rather than
|
|
1400
|
-
* discovering keys by scanning the environment. Scanning would have to guess how
|
|
1401
|
-
* `S3_BUCKET__MEDIA_CDN` splits into a key; deriving cannot be ambiguous, and a
|
|
1402
|
-
* typo surfaces as a missing source at boot instead of a silently ignored
|
|
1403
|
-
* variable.
|
|
1404
|
-
*
|
|
1405
|
-
* It lives in this package, with no dependencies, because four things must agree
|
|
1406
|
-
* on it exactly: the CLI (validating a build), the runtime (reading its own
|
|
1407
|
-
* environment), the control plane (writing a tenant's Secret), and the docs. A
|
|
1408
|
-
* second implementation of a naming convention is a second chance to disagree.
|
|
1409
|
-
*
|
|
1410
|
-
* @group Models
|
|
1411
|
-
*/
|
|
1412
|
-
function storageEnvSuffix(key, defaultKey = DEFAULT_STORAGE_SOURCE_KEY) {
|
|
1413
|
-
if (!key || key === defaultKey) return "";
|
|
1414
|
-
const normalized = key.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
|
|
1415
|
-
if (!normalized) throw new Error(`Source key "${key}" cannot be turned into an environment variable name. Use a key containing at least one letter or digit.`);
|
|
1416
|
-
return `__${normalized}`;
|
|
1417
|
-
}
|
|
1418
|
-
/**
|
|
1419
|
-
* Two distinct keys that collapse onto the same variable name, or `null`.
|
|
1420
|
-
*
|
|
1421
|
-
* `media-cdn` and `media_cdn` are different source keys but the same suffix, so
|
|
1422
|
-
* without this one of them silently reads the other's configuration. Returns the
|
|
1423
|
-
* offending pair rather than throwing, so each caller can raise it in its own
|
|
1424
|
-
* idiom — a `BundleError` at boot, a build failure in the CLI, a rejected deploy
|
|
1425
|
-
* in a control plane.
|
|
1426
|
-
*
|
|
1427
|
-
* @group Models
|
|
1428
|
-
*/
|
|
1429
|
-
function findStorageSuffixCollision(keys, defaultKey = DEFAULT_STORAGE_SOURCE_KEY) {
|
|
1430
|
-
const seen = /* @__PURE__ */ new Map();
|
|
1431
|
-
for (const key of keys) {
|
|
1432
|
-
const suffix = storageEnvSuffix(key, defaultKey);
|
|
1433
|
-
const existing = seen.get(suffix);
|
|
1434
|
-
if (existing !== void 0 && existing !== key) return {
|
|
1435
|
-
a: existing,
|
|
1436
|
-
b: key,
|
|
1437
|
-
suffix
|
|
1438
|
-
};
|
|
1439
|
-
seen.set(suffix, key);
|
|
1440
|
-
}
|
|
1441
|
-
return null;
|
|
1442
|
-
}
|
|
1443
|
-
/**
|
|
1444
|
-
* Merge the two places a project may declare storage sources into one list.
|
|
1445
|
-
*
|
|
1446
|
-
* `rebase.json` is authoritative for every field it states. Config code may add
|
|
1447
|
-
* sources it does not mention and fill in fields it left out, but may not
|
|
1448
|
-
* contradict it: the manifest is what a host reads to decide which buckets need
|
|
1449
|
-
* configuring, and a runtime that quietly disagreed with it would put the
|
|
1450
|
-
* console back to describing a topology the tenant does not have — the exact
|
|
1451
|
-
* failure this whole mechanism exists to end.
|
|
1452
|
-
*
|
|
1453
|
-
* Note what is *not* here: no default source is invented when both inputs are
|
|
1454
|
-
* empty. That decision belongs to the resolver, which knows whether declaring
|
|
1455
|
-
* nothing means "one plain bucket" (it does) or "no storage at all".
|
|
1456
|
-
*
|
|
1457
|
-
* @group Models
|
|
1458
|
-
*/
|
|
1459
|
-
function normalizeStorageSources(declared, exported) {
|
|
1460
|
-
const merged = /* @__PURE__ */ new Map();
|
|
1461
|
-
const declaredEntries = Array.isArray(declared) ? declared.filter((d) => d?.key).map((d) => [d.key, d]) : Object.entries(declared ?? {});
|
|
1462
|
-
for (const [key, config] of declaredEntries) merged.set(key, {
|
|
1463
|
-
key,
|
|
1464
|
-
engine: config.engine,
|
|
1465
|
-
transport: config.transport ?? "server",
|
|
1466
|
-
...config.label !== void 0 ? { label: config.label } : {}
|
|
1467
|
-
});
|
|
1468
|
-
for (const definition of exported ?? []) {
|
|
1469
|
-
if (!definition?.key) continue;
|
|
1470
|
-
const existing = merged.get(definition.key);
|
|
1471
|
-
if (!existing) {
|
|
1472
|
-
merged.set(definition.key, {
|
|
1473
|
-
key: definition.key,
|
|
1474
|
-
engine: definition.engine,
|
|
1475
|
-
transport: definition.transport ?? "server",
|
|
1476
|
-
...definition.label !== void 0 ? { label: definition.label } : {}
|
|
1477
|
-
});
|
|
1478
|
-
continue;
|
|
1479
|
-
}
|
|
1480
|
-
if (existing.label === void 0 && definition.label !== void 0) existing.label = definition.label;
|
|
1481
|
-
}
|
|
1482
|
-
return Array.from(merged.values());
|
|
1842
|
+
/** The declared buckets, as definitions. */
|
|
1843
|
+
function declaredStorageSources() {
|
|
1844
|
+
return declaredResources("bucket").map(resourceToStorageSource);
|
|
1483
1845
|
}
|
|
1484
1846
|
//#endregion
|
|
1485
1847
|
//#region src/types/component_ref.ts
|
|
@@ -1893,6 +2255,6 @@ function isPublicStoragePath(path) {
|
|
|
1893
2255
|
return p.startsWith("public/") || p.startsWith(`default/public/`);
|
|
1894
2256
|
}
|
|
1895
2257
|
//#endregion
|
|
1896
|
-
export { ADMIN_COLLECTION_KEYS, ADMIN_PROPERTY_KEYS, ALL_WHERE_FILTER_OPS, ANONYMOUS_USER_ID, ANONYMOUS_USER_IDS, BUNDLE_FORMAT_VERSION, CANONICAL_TO_REST, DEFAULT_CAPABILITIES, DEFAULT_COMMIT_PATHS, DEFAULT_DATA_SOURCE_KEY, DEFAULT_FILTERABLE_RELATION_KINDS, DEFAULT_FUZZY_THRESHOLD, DEFAULT_LIST_LIMIT, DEFAULT_RESOURCE_KEY, DEFAULT_SEARCH_COLUMN, DEFAULT_SEARCH_LANGUAGE, DEFAULT_SEARCH_WEIGHT, DEFAULT_STORAGE_SOURCE_KEY, DEFAULT_VECTOR_LIST_LIMIT, EntityReference, EntityRelation, FIREBASE_CAPABILITIES, GeoPoint, LEGACY_RLS_JWT_SQL, LEGACY_RLS_ROLES_SQL, LEGACY_RLS_SCHEMA, LEGACY_RLS_UID_SQL, ListLimitError, MAX_LIST_LIMIT, MONGODB_CAPABILITIES, NULL_OPS, POSTGRES_CAPABILITIES, PUBLIC_STORAGE_PREFIX, REBASE_SCHEMA, RELEVANCE_SORT_FIELD, RESERVED_BACKEND_PREFIXES, RESOURCE_GRAPH_VERSION, REST_TO_CANONICAL, RLS_JWT_SQL, RLS_ROLES_SQL, RLS_UID_SQL, RUNTIME_CONTRACT_VERSION, RebaseApiError, RebaseClientError, SCHEMA_VERSION_HEADER, Vector, bucket, buildResourceGraph, canonicalSchemaPayload, computeSchemaVersion, database, declareResource, declaredDataSources, declaredDatabaseExtensions, declaredResources, declaredStorageSources, declaredSubscriptions, deserializeCollections, encodeRelationAggregateSort, envBasesForResource, findEnvSuffixCollision, findStorageSuffixCollision, getCollectionDataPath, getDataSourceCapabilities, getDeclaredSubcollections, hasForeignKeyOnTarget, isAnonymousUid, isBranchAdmin, isChannelBusInstance, isDocumentAdmin, isFirebaseCollectionConfig, isLazyComponentRef, isManyToMany, isMongoDBCollectionConfig, isPostgresCollectionConfig, isPublicStoragePath, isRelationAggregateSort, isRelationalCollectionConfig, isResourceHandle, isSQLAdmin, isSchemaAdmin, isSchemaEditingAdmin, isSerializedCollectionRef, isToMany, isValidEngine, nestAdminCollectionKeys, nestAdminKeysOf, nestAdminPropertyKeys,
|
|
2258
|
+
export { ADMIN_COLLECTION_KEYS, ADMIN_PROPERTY_KEYS, ALL_WHERE_FILTER_OPS, ANONYMOUS_USER_ID, ANONYMOUS_USER_IDS, BUNDLE_FORMAT_VERSION, CANONICAL_TO_REST, DEFAULT_CAPABILITIES, DEFAULT_COMMIT_PATHS, DEFAULT_DATA_SOURCE_KEY, DEFAULT_FILTERABLE_RELATION_KINDS, DEFAULT_FUZZY_THRESHOLD, DEFAULT_LIST_LIMIT, DEFAULT_RESOURCE_KEY, DEFAULT_SEARCH_COLUMN, DEFAULT_SEARCH_LANGUAGE, DEFAULT_SEARCH_WEIGHT, DEFAULT_STORAGE_SOURCE_KEY, DEFAULT_VECTOR_LIST_LIMIT, EntityReference, EntityRelation, FIREBASE_CAPABILITIES, GeoPoint, LEGACY_RLS_JWT_SQL, LEGACY_RLS_ROLES_SQL, LEGACY_RLS_SCHEMA, LEGACY_RLS_UID_SQL, ListLimitError, MAX_LIST_LIMIT, MONGODB_CAPABILITIES, NULL_OPS, POSTGRES_CAPABILITIES, PUBLIC_STORAGE_PREFIX, REBASE_SCHEMA, RELEVANCE_SORT_FIELD, RESERVED_BACKEND_PREFIXES, RESOURCE_GRAPH_VERSION, REST_TO_CANONICAL, RLS_IS_ANONYMOUS_SQL, RLS_JWT_SQL, RLS_ROLES_SQL, RLS_UID_SQL, RUNTIME_CONTRACT_VERSION, RebaseApiError, RebaseClientError, SCHEMA_VERSION_HEADER, Vector, amendResourceKind, bucket, buildResourceGraph, canonicalSchemaPayload, computeSchemaVersion, database, declareCron, declareFunction, declareResource, declaredDataSources, declaredDatabaseExtensions, declaredQueueConsumers, declaredResources, declaredStorageSources, declaredSubscriptions, deserializeCollections, encodeRelationAggregateSort, envBasesForResource, findEnvSuffixCollision, findStorageSuffixCollision, getCollectionDataPath, getDataSourceCapabilities, getDeclaredSubcollections, hasForeignKeyOnTarget, isAnonymousUid, isBranchAdmin, isChannelBusInstance, isDocumentAdmin, isFirebaseCollectionConfig, isLazyComponentRef, isManyToMany, isMongoDBCollectionConfig, isPostgresCollectionConfig, isPublicStoragePath, isRelationAggregateSort, isRelationalCollectionConfig, isResourceHandle, isSQLAdmin, isSchemaAdmin, isSchemaEditingAdmin, isSerializedCollectionRef, isToMany, isUnsupported, isValidEngine, nestAdminCollectionKeys, nestAdminKeysOf, nestAdminPropertyKeys, parseRelationAggregateSort, policy, queue, registerDataSourceCapabilities, registerResourceKind, reservedPrefixFor, resetDeclaredQueueConsumers, resetDeclaredResources, resetDeclaredSubscriptions, resolveClientListLimit, resolveResourceRefs, resourceEnvSuffix, resourceId, resourceKeyOf, resourceKind, resourceKinds, resourceToDataSource, resourceToStorageSource, rewriteLegacyRlsFunctions, serializeCollections, setQueueRuntime, setTopicRuntime, sortKeyToString, storageEnvSuffix, toCanonicalOp, topic, unsupportedMethod, usesLegacyRlsFunctions };
|
|
1897
2259
|
|
|
1898
2260
|
//# sourceMappingURL=index.es.js.map
|