@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/src/types/resources.ts
DELETED
|
@@ -1,368 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The resource graph: one declaration site for every named thing a project needs.
|
|
3
|
-
*
|
|
4
|
-
* ## The rule
|
|
5
|
-
*
|
|
6
|
-
* **Every named resource is declared with a constructor in config code.** A
|
|
7
|
-
* database, a bucket, a topic and whatever kind comes next are all spelled the
|
|
8
|
-
* same way, so "where do I declare my second one" has one answer instead of one
|
|
9
|
-
* answer per kind.
|
|
10
|
-
*
|
|
11
|
-
* ```ts
|
|
12
|
-
* export const main = database("main");
|
|
13
|
-
* export const media = bucket("media", { transport: "direct" });
|
|
14
|
-
* export const signups = topic<SignupEvent>("signups");
|
|
15
|
-
* ```
|
|
16
|
-
*
|
|
17
|
-
* ## Declaration is not binding
|
|
18
|
-
*
|
|
19
|
-
* A declaration says a resource *exists* and what shape it has. It never says
|
|
20
|
-
* how to reach it — that is a property of the environment, not of the project,
|
|
21
|
-
* and it differs between a laptop, a self-hosted box and a tenant in the cloud.
|
|
22
|
-
* Binding lives in `@rebasepro/server`'s boot path, reading environment
|
|
23
|
-
* variables or an infrastructure config file, and it keys off the logical name
|
|
24
|
-
* declared here.
|
|
25
|
-
*
|
|
26
|
-
* This split is the whole point. Before it, storage topology was hand-written
|
|
27
|
-
* into `rebase.json` while database topology lived in TypeScript, and the
|
|
28
|
-
* boundary between them was a fact about what the control plane could read
|
|
29
|
-
* before a build — a platform implementation detail that a developer had no way
|
|
30
|
-
* to derive. Worse, storage could be declared in *both* places, and the merge
|
|
31
|
-
* silently kept the JSON's engine and discarded the code's.
|
|
32
|
-
*
|
|
33
|
-
* ## Why a registry rather than a fixed union
|
|
34
|
-
*
|
|
35
|
-
* Kinds register themselves. Adding pub/sub, a cache or a search index must not
|
|
36
|
-
* require editing a manifest schema, a validator and three switch statements —
|
|
37
|
-
* that cost is exactly why the last two kinds ended up in different homes.
|
|
38
|
-
*/
|
|
39
|
-
|
|
40
|
-
/** How a client reaches a resource. */
|
|
41
|
-
export type ResourceTransport =
|
|
42
|
-
/** Through the backend. The default, and the only one that needs no client SDK. */
|
|
43
|
-
| "server"
|
|
44
|
-
/** A provider SDK talks to the resource directly; the backend is not in the path. */
|
|
45
|
-
| "direct";
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* A resource kind, as registered.
|
|
49
|
-
*
|
|
50
|
-
* `engines` is an allowlist rather than documentation. An unrecognised engine
|
|
51
|
-
* used to be a free string that passed every check and failed later, further
|
|
52
|
-
* from the typo that caused it — `"s2"` for `"s3"` reached the runtime. Anything
|
|
53
|
-
* genuinely outside the list is spelled `custom:<id>`, which says so at the call
|
|
54
|
-
* site instead of looking like a typo.
|
|
55
|
-
*/
|
|
56
|
-
export interface ResourceKindSpec {
|
|
57
|
-
/** The kind's name, as it appears in a declaration and in the graph. */
|
|
58
|
-
kind: string;
|
|
59
|
-
/** Engines this kind ships with. `custom:<id>` is always additionally valid. */
|
|
60
|
-
engines: readonly string[];
|
|
61
|
-
/** Used when a declaration names none. */
|
|
62
|
-
defaultEngine: string;
|
|
63
|
-
/**
|
|
64
|
-
* Environment variable base names this kind binds from, in the order a
|
|
65
|
-
* binder should try them. A resource keyed `analytics` reads
|
|
66
|
-
* `<BASE>__ANALYTICS`; the default-keyed resource reads `<BASE>` unsuffixed,
|
|
67
|
-
* so a single-resource project configured the obvious way declares nothing.
|
|
68
|
-
*/
|
|
69
|
-
envBases: readonly string[];
|
|
70
|
-
/**
|
|
71
|
-
* The subset of `envBases` that matters for a given engine.
|
|
72
|
-
*
|
|
73
|
-
* The binder reads every base and takes whichever is set — harmless, and it
|
|
74
|
-
* keeps binding tolerant. A GENERATOR cannot be that relaxed: `rebase eject
|
|
75
|
-
* infra` writing S3_BUCKET, GCS_BUCKET, STORAGE_BUCKET and
|
|
76
|
-
* STORAGE_PUBLIC_URL for a `local` bucket hands somebody four variables of
|
|
77
|
-
* which three are noise, and a config file full of irrelevant keys is one
|
|
78
|
-
* nobody reads carefully.
|
|
79
|
-
*
|
|
80
|
-
* Keyed by engine; an engine with no entry falls back to all of them, which
|
|
81
|
-
* is the honest answer for one this package has never heard of.
|
|
82
|
-
*/
|
|
83
|
-
envBasesByEngine?: Readonly<Record<string, readonly string[]>>;
|
|
84
|
-
/** Option keys this kind accepts beyond the common ones, for validation. */
|
|
85
|
-
optionKeys?: readonly string[];
|
|
86
|
-
/**
|
|
87
|
-
* Whether a project implicitly has one of these even when it declares
|
|
88
|
-
* nothing. True for databases — a backend without one is not a backend —
|
|
89
|
-
* and false for topics, where zero is the normal number.
|
|
90
|
-
*/
|
|
91
|
-
implicitDefault?: boolean;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** The key a resource takes when a project declares only one of its kind. */
|
|
95
|
-
export const DEFAULT_RESOURCE_KEY = "(default)";
|
|
96
|
-
|
|
97
|
-
/** A declared resource, as it appears in the graph. */
|
|
98
|
-
export interface ResourceDeclaration {
|
|
99
|
-
kind: string;
|
|
100
|
-
/** Unique within its kind. What a binder looks up and what an env suffix is built from. */
|
|
101
|
-
key: string;
|
|
102
|
-
engine: string;
|
|
103
|
-
transport: ResourceTransport;
|
|
104
|
-
label?: string;
|
|
105
|
-
/** Kind-specific options, validated against the kind's `optionKeys`. */
|
|
106
|
-
options: Readonly<Record<string, unknown>>;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* The value a constructor returns.
|
|
111
|
-
*
|
|
112
|
-
* Carries its own declaration so config code can hold it and pass it around,
|
|
113
|
-
* and stringifies to its key so it drops into the places that still take one.
|
|
114
|
-
* Collections name a data source by string today; a handle works there without
|
|
115
|
-
* the collection API having to change, which keeps this a config redesign
|
|
116
|
-
* rather than a rewrite of the data layer.
|
|
117
|
-
*/
|
|
118
|
-
export interface ResourceHandle extends ResourceDeclaration {
|
|
119
|
-
toString(): string;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const BRAND = Symbol.for("@rebasepro/types.resource");
|
|
123
|
-
|
|
124
|
-
/** Whether a value is a resource handle rather than a plain string key. */
|
|
125
|
-
export function isResourceHandle(value: unknown): value is ResourceHandle {
|
|
126
|
-
return typeof value === "object" && value !== null && BRAND in value;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** The key a resource reference names, whether it is a handle or already a key. */
|
|
130
|
-
export function resourceKeyOf(ref: string | ResourceHandle): string {
|
|
131
|
-
return isResourceHandle(ref) ? ref.key : ref;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* The process-wide registry.
|
|
136
|
-
*
|
|
137
|
-
* Keyed off `globalThis` through a shared symbol rather than held in a module
|
|
138
|
-
* local, because a module local is per *copy* of this package. A project that
|
|
139
|
-
* ends up with two copies of `@rebasepro/types` — which a partially-linked
|
|
140
|
-
* `node_modules` produces, and which has already caused a phantom
|
|
141
|
-
* "JWT secret not configured" bug in this repo — would otherwise register into
|
|
142
|
-
* one registry and read from the other, and see an empty graph with nothing
|
|
143
|
-
* anywhere to explain it.
|
|
144
|
-
*/
|
|
145
|
-
interface Registry {
|
|
146
|
-
kinds: Map<string, ResourceKindSpec>;
|
|
147
|
-
declarations: Map<string, ResourceDeclaration>;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const GLOBAL_KEY = Symbol.for("@rebasepro/types.resourceRegistry");
|
|
151
|
-
|
|
152
|
-
function registry(): Registry {
|
|
153
|
-
const g = globalThis as unknown as Record<symbol, Registry | undefined>;
|
|
154
|
-
let existing = g[GLOBAL_KEY];
|
|
155
|
-
if (!existing) {
|
|
156
|
-
existing = { kinds: new Map(), declarations: new Map() };
|
|
157
|
-
g[GLOBAL_KEY] = existing;
|
|
158
|
-
}
|
|
159
|
-
return existing;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/** `kind:key`, the graph's primary key. */
|
|
163
|
-
function declarationId(kind: string, key: string): string {
|
|
164
|
-
return `${kind}:${key}`;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** Register a resource kind. Idempotent for an identical spec; throws on a conflicting one. */
|
|
168
|
-
export function registerResourceKind(spec: ResourceKindSpec): void {
|
|
169
|
-
const existing = registry().kinds.get(spec.kind);
|
|
170
|
-
if (existing && JSON.stringify(existing) !== JSON.stringify(spec)) {
|
|
171
|
-
throw new Error(
|
|
172
|
-
`Resource kind "${spec.kind}" is already registered with a different definition. ` +
|
|
173
|
-
"Two packages cannot define the same kind."
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
registry().kinds.set(spec.kind, spec);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** Every registered kind, for validators and for `rebase doctor`. */
|
|
180
|
-
export function resourceKinds(): ResourceKindSpec[] {
|
|
181
|
-
return [...registry().kinds.values()];
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/** One registered kind, or undefined. */
|
|
185
|
-
export function resourceKind(kind: string): ResourceKindSpec | undefined {
|
|
186
|
-
return registry().kinds.get(kind);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** Options every kind accepts. */
|
|
190
|
-
export interface DeclareOptions {
|
|
191
|
-
engine?: string;
|
|
192
|
-
transport?: ResourceTransport;
|
|
193
|
-
label?: string;
|
|
194
|
-
[option: string]: unknown;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const COMMON_OPTION_KEYS = ["engine", "transport", "label"] as const;
|
|
198
|
-
|
|
199
|
-
/** Whether an engine is one the kind knows, or an explicit `custom:` opt-out. */
|
|
200
|
-
export function isValidEngine(spec: ResourceKindSpec, engine: string): boolean {
|
|
201
|
-
return engine.startsWith("custom:") || spec.engines.includes(engine);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Declare a resource. The primitive every kind's constructor is built from.
|
|
206
|
-
*
|
|
207
|
-
* Redeclaring the same `kind:key` with a *different* shape throws rather than
|
|
208
|
-
* merging. Merging is what the old storage path did, and it silently discarded
|
|
209
|
-
* one of the two engines — a declaration accepted and then ignored, which is
|
|
210
|
-
* the failure this whole model exists to remove. Redeclaring it identically is
|
|
211
|
-
* fine: a config module evaluated twice must not be an error.
|
|
212
|
-
*/
|
|
213
|
-
export function declareResource(
|
|
214
|
-
kind: string,
|
|
215
|
-
key: string = DEFAULT_RESOURCE_KEY,
|
|
216
|
-
options: DeclareOptions = {}
|
|
217
|
-
): ResourceHandle {
|
|
218
|
-
const spec = registry().kinds.get(kind);
|
|
219
|
-
if (!spec) {
|
|
220
|
-
const known = [...registry().kinds.keys()].sort().join(", ") || "none";
|
|
221
|
-
throw new Error(
|
|
222
|
-
`Unknown resource kind "${kind}". Registered kinds: ${known}. ` +
|
|
223
|
-
"Call registerResourceKind() before declaring one."
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (!key || typeof key !== "string" || key.trim() === "") {
|
|
228
|
-
throw new Error(`A ${kind} needs a non-empty key.`);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const engine = options.engine ?? spec.defaultEngine;
|
|
232
|
-
if (!isValidEngine(spec, engine)) {
|
|
233
|
-
throw new Error(
|
|
234
|
-
`Unknown ${kind} engine "${engine}" for "${key}". ` +
|
|
235
|
-
`Known engines: ${spec.engines.join(", ")}. ` +
|
|
236
|
-
`An engine this build does not ship is spelled "custom:${engine}", ` +
|
|
237
|
-
"which says so at the call site rather than failing later."
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const allowed = new Set<string>([...COMMON_OPTION_KEYS, ...(spec.optionKeys ?? [])]);
|
|
242
|
-
const unknown = Object.keys(options).filter(k => !allowed.has(k));
|
|
243
|
-
if (unknown.length > 0) {
|
|
244
|
-
throw new Error(
|
|
245
|
-
`Unknown option(s) on ${kind} "${key}": ${unknown.join(", ")}. ` +
|
|
246
|
-
`A ${kind} accepts: ${[...allowed].sort().join(", ")}.`
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const extra: Record<string, unknown> = {};
|
|
251
|
-
for (const k of spec.optionKeys ?? []) {
|
|
252
|
-
if (options[k] !== undefined) extra[k] = options[k];
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const declaration: ResourceDeclaration = {
|
|
256
|
-
kind,
|
|
257
|
-
key,
|
|
258
|
-
engine,
|
|
259
|
-
transport: options.transport ?? "server",
|
|
260
|
-
...(options.label !== undefined ? { label: options.label } : {}),
|
|
261
|
-
options: Object.freeze(extra)
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
const id = declarationId(kind, key);
|
|
265
|
-
const previous = registry().declarations.get(id);
|
|
266
|
-
if (previous) {
|
|
267
|
-
if (JSON.stringify(previous) !== JSON.stringify(declaration)) {
|
|
268
|
-
throw new Error(
|
|
269
|
-
`${kind} "${key}" is declared twice with different configuration. ` +
|
|
270
|
-
"Declare it once and export it — two declarations of one resource is " +
|
|
271
|
-
"the ambiguity this model exists to remove, so it is refused rather " +
|
|
272
|
-
"than merged."
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
} else {
|
|
276
|
-
registry().declarations.set(id, declaration);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const handle = {
|
|
280
|
-
...declaration,
|
|
281
|
-
toString() { return key; },
|
|
282
|
-
[BRAND]: true as const
|
|
283
|
-
};
|
|
284
|
-
return handle as ResourceHandle;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/** Every declared resource, in declaration order, optionally filtered by kind. */
|
|
288
|
-
export function declaredResources(kind?: string): ResourceDeclaration[] {
|
|
289
|
-
const all = [...registry().declarations.values()];
|
|
290
|
-
return kind ? all.filter(r => r.kind === kind) : all;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Forget every declaration, keeping registered kinds.
|
|
295
|
-
*
|
|
296
|
-
* For tests and for a CLI that evaluates more than one project in a process.
|
|
297
|
-
* Kinds survive because they are registered by module import, which will not
|
|
298
|
-
* happen a second time.
|
|
299
|
-
*/
|
|
300
|
-
export function resetDeclaredResources(): void {
|
|
301
|
-
registry().declarations.clear();
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* The env-var suffix a resource's bindings use: `__ANALYTICS` for `analytics`,
|
|
306
|
-
* and nothing at all for the default-keyed one.
|
|
307
|
-
*
|
|
308
|
-
* The default takes no suffix so that a project with one database configured
|
|
309
|
-
* through plain `DATABASE_URL` keeps working having declared nothing — the
|
|
310
|
-
* overwhelmingly common project must not have to say so.
|
|
311
|
-
*/
|
|
312
|
-
export function resourceEnvSuffix(key: string): string {
|
|
313
|
-
if (key === DEFAULT_RESOURCE_KEY) return "";
|
|
314
|
-
return `__${key.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "")}`;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Two resources of a kind whose keys differ but whose env suffixes do not.
|
|
319
|
-
*
|
|
320
|
-
* `media-files` and `media_files` both become `__MEDIA_FILES`, so one would
|
|
321
|
-
* silently read the other's configuration. Returned rather than thrown so the
|
|
322
|
-
* caller can report it with the rest of a validation pass.
|
|
323
|
-
*/
|
|
324
|
-
export function findEnvSuffixCollision(keys: readonly string[]): { a: string; b: string; suffix: string } | null {
|
|
325
|
-
const seen = new Map<string, string>();
|
|
326
|
-
for (const key of keys) {
|
|
327
|
-
const suffix = resourceEnvSuffix(key);
|
|
328
|
-
const previous = seen.get(suffix);
|
|
329
|
-
if (previous !== undefined && previous !== key) return { a: previous, b: key, suffix };
|
|
330
|
-
seen.set(suffix, key);
|
|
331
|
-
}
|
|
332
|
-
return null;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* The whole graph, as recorded in a manifest and read by a host.
|
|
337
|
-
*
|
|
338
|
-
* `version` is the graph format, not the project's. A host reading a graph it
|
|
339
|
-
* does not understand must say so rather than provision half of it.
|
|
340
|
-
*/
|
|
341
|
-
export interface ResourceGraph {
|
|
342
|
-
version: 1;
|
|
343
|
-
resources: ResourceDeclaration[];
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/** The current graph format version. */
|
|
347
|
-
export const RESOURCE_GRAPH_VERSION = 1 as const;
|
|
348
|
-
|
|
349
|
-
/** Build a graph from the current declarations, sorted for a stable diff. */
|
|
350
|
-
export function buildResourceGraph(): ResourceGraph {
|
|
351
|
-
const resources = declaredResources().slice().sort(
|
|
352
|
-
(a, b) => a.kind.localeCompare(b.kind) || a.key.localeCompare(b.key)
|
|
353
|
-
);
|
|
354
|
-
return { version: RESOURCE_GRAPH_VERSION, resources };
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* The environment variables worth writing for a resource, given its engine.
|
|
359
|
-
*
|
|
360
|
-
* Falls back to every base the kind reads when the engine is unknown — a
|
|
361
|
-
* `custom:` engine gets the full list rather than an empty one, because
|
|
362
|
-
* guessing narrow would silently omit the variable it actually needs.
|
|
363
|
-
*/
|
|
364
|
-
export function envBasesForResource(declaration: ResourceDeclaration): readonly string[] {
|
|
365
|
-
const spec = resourceKind(declaration.kind);
|
|
366
|
-
if (!spec) return [];
|
|
367
|
-
return spec.envBasesByEngine?.[declaration.engine] ?? spec.envBases;
|
|
368
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The SQL helper functions RLS policies call, and the schema they live in.
|
|
3
|
-
*
|
|
4
|
-
* ## One schema, and it is ours
|
|
5
|
-
*
|
|
6
|
-
* Rebase creates exactly one schema in a project's database: `rebase`. These
|
|
7
|
-
* three functions live in it alongside the framework's own tables, and that is
|
|
8
|
-
* the whole contract — a reader can look at a database and know precisely which
|
|
9
|
-
* namespace belongs to the framework and that nothing else was touched.
|
|
10
|
-
*
|
|
11
|
-
* It used to be two. `uid()`, `jwt()` and `roles()` sat in a schema called
|
|
12
|
-
* `auth`, which is Supabase's name, chosen so that a developer who had written
|
|
13
|
-
* Supabase RLS would recognise `auth.uid()`. The familiarity was real but the
|
|
14
|
-
* name was not Rebase's to take, and taking it had a concrete cost: pointing
|
|
15
|
-
* Rebase at a database that already had a Supabase `auth` schema meant
|
|
16
|
-
* `CREATE OR REPLACE FUNCTION auth.uid() RETURNS text` against Supabase's
|
|
17
|
-
* `RETURNS uuid`, which Postgres rejects outright —
|
|
18
|
-
*
|
|
19
|
-
* ERROR: cannot change return type of existing function
|
|
20
|
-
* HINT: Use DROP FUNCTION auth.uid() first.
|
|
21
|
-
*
|
|
22
|
-
* — and the failure landed inside a catch-all that logged a warning and carried
|
|
23
|
-
* on, leaving a database with auth tables, no helper functions, and policies
|
|
24
|
-
* calling functions that did not exist. Under `rebase db migrate` the same
|
|
25
|
-
* statements aborted the migration instead.
|
|
26
|
-
*
|
|
27
|
-
* `rebase.uid()` collides with nobody. A Supabase database keeps its `auth`
|
|
28
|
-
* schema untouched and gains a `rebase` one, which is what a gradual migration
|
|
29
|
-
* needs.
|
|
30
|
-
*
|
|
31
|
-
* ## Why functions at all, rather than inlining `current_setting`
|
|
32
|
-
*
|
|
33
|
-
* Because the indirection has already been spent once. `uid()` resolves
|
|
34
|
-
* `app.uid` and falls back to the pre-rename `app.user_id`, so that during a
|
|
35
|
-
* rolling deploy — old and new pods serving one database — both eras resolve
|
|
36
|
-
* the principal. That was a single `CREATE OR REPLACE`. Inlined into policy
|
|
37
|
-
* bodies it would have been a rewrite of every policy on every table.
|
|
38
|
-
*
|
|
39
|
-
* ## Why the name is not configurable
|
|
40
|
-
*
|
|
41
|
-
* A policy body is stored SQL: Postgres parses `USING (…)` once and keeps it, so
|
|
42
|
-
* these strings are written into every policy in every database Rebase has
|
|
43
|
-
* provisioned. Everything that reads policies back — the SQL-to-policy parser
|
|
44
|
-
* behind the admin UI, the drift checker, `rls-check` — would have to know the
|
|
45
|
-
* configured value to recognise its own output. One frozen name is the feature.
|
|
46
|
-
*/
|
|
47
|
-
|
|
48
|
-
/** The schema Rebase owns. The only schema Rebase creates. */
|
|
49
|
-
export const REBASE_SCHEMA = "rebase";
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* The principal of the current request, as text, or NULL in the server context.
|
|
53
|
-
*
|
|
54
|
-
* Never NULL for a user request — an anonymous one carries
|
|
55
|
-
* {@link ANONYMOUS_USER_ID} — which is what makes `IS NULL` a reliable test for
|
|
56
|
-
* the trusted server plane and `IS NOT NULL` a tautology.
|
|
57
|
-
*/
|
|
58
|
-
export const RLS_UID_SQL = `${REBASE_SCHEMA}.uid()`;
|
|
59
|
-
|
|
60
|
-
/** The request's roles as a comma-separated string, for `string_to_array`. */
|
|
61
|
-
export const RLS_ROLES_SQL = `${REBASE_SCHEMA}.roles()`;
|
|
62
|
-
|
|
63
|
-
/** The request's JWT claims as `jsonb`, or `{}`. */
|
|
64
|
-
export const RLS_JWT_SQL = `${REBASE_SCHEMA}.jwt()`;
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* The pre-1.0 spellings, for recognising policies and hand-written SQL that
|
|
68
|
-
* predate the move.
|
|
69
|
-
*
|
|
70
|
-
* Kept because policies outlive the server that wrote them: a database migrated
|
|
71
|
-
* by an older release still holds `auth.uid()` in its policy bodies until the
|
|
72
|
-
* next push or boot recompiles them, and anything that reads policies back has
|
|
73
|
-
* to recognise both eras or report the framework's own output as foreign drift.
|
|
74
|
-
* Also used to give a project whose `securityRules` contain raw `auth.uid()` a
|
|
75
|
-
* message naming the replacement, instead of a parse failure.
|
|
76
|
-
*/
|
|
77
|
-
export const LEGACY_RLS_SCHEMA = "auth";
|
|
78
|
-
export const LEGACY_RLS_UID_SQL = `${LEGACY_RLS_SCHEMA}.uid()`;
|
|
79
|
-
export const LEGACY_RLS_ROLES_SQL = `${LEGACY_RLS_SCHEMA}.roles()`;
|
|
80
|
-
export const LEGACY_RLS_JWT_SQL = `${LEGACY_RLS_SCHEMA}.jwt()`;
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Rewrites the pre-1.0 function calls in a fragment of policy SQL.
|
|
84
|
-
*
|
|
85
|
-
* Deliberately anchored on a word boundary and the schema qualifier, so a column
|
|
86
|
-
* called `auth_uid` or a table named `auth` is left alone.
|
|
87
|
-
*/
|
|
88
|
-
export function rewriteLegacyRlsFunctions(sql: string): string {
|
|
89
|
-
return sql.replace(
|
|
90
|
-
/\bauth\.(uid|jwt|roles)\s*\(\s*\)/gi,
|
|
91
|
-
(_match, fn: string) => `${REBASE_SCHEMA}.${fn.toLowerCase()}()`
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Whether a fragment of SQL still calls the pre-1.0 functions. */
|
|
96
|
-
export function usesLegacyRlsFunctions(sql: string): boolean {
|
|
97
|
-
return /\bauth\.(uid|jwt|roles)\s*\(\s*\)/i.test(sql);
|
|
98
|
-
}
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The vocabulary a live schema change is described in.
|
|
3
|
-
*
|
|
4
|
-
* Declared here, and nowhere else, because two packages that must not import
|
|
5
|
-
* each other both need it: `@rebasepro/server-postgres` decides what a change
|
|
6
|
-
* means and renders the files it needs, while `@rebasepro/server` commits those
|
|
7
|
-
* files and serves the routes. Neither can reach the other — the server is
|
|
8
|
-
* engine-agnostic by design — so the shared kernel holds the shapes and the
|
|
9
|
-
* driver is detected structurally through {@link SchemaEditingAdmin}.
|
|
10
|
-
*
|
|
11
|
-
* Nothing here executes anything. These are the nouns.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* What a change will do to a live database.
|
|
16
|
-
*
|
|
17
|
-
* - `safe` — the boot-time ensure path expresses it, and the result matches the
|
|
18
|
-
* configuration.
|
|
19
|
-
* - `diverges` — the ensure path applies *something*, but the database will not
|
|
20
|
-
* match what the configuration declares, and nothing reports it. This is the
|
|
21
|
-
* category worth having: adding a required property to a populated table
|
|
22
|
-
* yields a nullable column, and adding a value to an existing enum yields
|
|
23
|
-
* nothing at all. Both read as success.
|
|
24
|
-
* - `needs-migration` — the ensure path cannot express it. Dropping anything,
|
|
25
|
-
* changing a type, moving a primary key.
|
|
26
|
-
*/
|
|
27
|
-
export type SchemaChangeVerdict = "safe" | "diverges" | "needs-migration";
|
|
28
|
-
|
|
29
|
-
export type SchemaChangeKind =
|
|
30
|
-
| "add-collection"
|
|
31
|
-
| "remove-collection"
|
|
32
|
-
| "add-property"
|
|
33
|
-
| "remove-property"
|
|
34
|
-
| "change-property-type"
|
|
35
|
-
| "rename-column"
|
|
36
|
-
| "add-enum-value"
|
|
37
|
-
| "remove-enum-value"
|
|
38
|
-
| "change-required"
|
|
39
|
-
| "change-primary-key";
|
|
40
|
-
|
|
41
|
-
export interface SchemaChange {
|
|
42
|
-
kind: SchemaChangeKind;
|
|
43
|
-
verdict: SchemaChangeVerdict;
|
|
44
|
-
/** Collection slug. */
|
|
45
|
-
collection: string;
|
|
46
|
-
/** Property name, where the change is to one. */
|
|
47
|
-
property?: string;
|
|
48
|
-
/** One line, specific: what changed and what it will do. */
|
|
49
|
-
detail: string;
|
|
50
|
-
/** What to do instead, when the verdict is not `safe`. */
|
|
51
|
-
remedy?: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface ClassifiedSchemaChanges {
|
|
55
|
-
changes: SchemaChange[];
|
|
56
|
-
/** The worst verdict present, or `safe` for an empty diff. */
|
|
57
|
-
verdict: SchemaChangeVerdict;
|
|
58
|
-
/** True only when every change is `safe` — the one case an editor may apply. */
|
|
59
|
-
applicable: boolean;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Where a project's generated schema artifacts live, relative to the **project**
|
|
64
|
-
* root — which is the repository root only when the project is the whole
|
|
65
|
-
* repository.
|
|
66
|
-
*
|
|
67
|
-
* Here rather than in the Postgres package because it is a contract, not an
|
|
68
|
-
* engine detail: `@rebasepro/server` has to derive these for a project in a
|
|
69
|
-
* subdirectory, and it cannot import a driver to do it.
|
|
70
|
-
*/
|
|
71
|
-
export interface SchemaCommitPaths {
|
|
72
|
-
/** Drizzle schema, imported by the backend. */
|
|
73
|
-
schemaFile: string;
|
|
74
|
-
/** Declarative DDL, what `db push` applies and Atlas diffs against. */
|
|
75
|
-
ddlFile: string;
|
|
76
|
-
policiesFile: string;
|
|
77
|
-
searchFile: string;
|
|
78
|
-
/** Vector columns and ANN indexes — like search, applied by Rebase not Atlas. */
|
|
79
|
-
vectorFile: string;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export const DEFAULT_COMMIT_PATHS: SchemaCommitPaths = {
|
|
83
|
-
schemaFile: "backend/src/schema.generated.ts",
|
|
84
|
-
ddlFile: "drizzle/schema.sql",
|
|
85
|
-
policiesFile: "drizzle/policies.sql",
|
|
86
|
-
searchFile: "drizzle/search.sql",
|
|
87
|
-
vectorFile: "drizzle/vector.sql"
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
/** One file the commit writes, as content rather than as a path on a disk. */
|
|
91
|
-
export interface SchemaChangeFile {
|
|
92
|
-
path: string;
|
|
93
|
-
contents: string;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Everything a change needs written and run.
|
|
98
|
-
*
|
|
99
|
-
* Computed without touching a disk or a network. The database is *read* — what
|
|
100
|
-
* a change means depends on what is already there, and a plan that guessed
|
|
101
|
-
* would be guessing about whether the statements it returns will be accepted.
|
|
102
|
-
*/
|
|
103
|
-
export interface SchemaChangePlan {
|
|
104
|
-
/** Every file the commit writes — collection source and generated artifacts. */
|
|
105
|
-
files: SchemaChangeFile[];
|
|
106
|
-
/** The additive DDL this change adds, in dependency order. */
|
|
107
|
-
statements: string[];
|
|
108
|
-
classified: ClassifiedSchemaChanges;
|
|
109
|
-
/** A commit message describing the change rather than announcing one. */
|
|
110
|
-
message: string;
|
|
111
|
-
/**
|
|
112
|
-
* Constraints the configuration asks for that these statements do not
|
|
113
|
-
* carry, and why.
|
|
114
|
-
*
|
|
115
|
-
* Almost always empty. When it is not, it is the part the person confirming
|
|
116
|
-
* needs to read: the change will apply, and the database will still not
|
|
117
|
-
* enforce something the configuration says — a required property over a
|
|
118
|
-
* table that already holds rows with no value for it. Optional so a plan
|
|
119
|
-
* from an engine that does not distinguish these cases stays valid.
|
|
120
|
-
*/
|
|
121
|
-
withheldConstraints?: WithheldSchemaConstraint[];
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** A constraint a plan asks for and does not apply. */
|
|
125
|
-
export interface WithheldSchemaConstraint {
|
|
126
|
-
/** `schema.table.column`. */
|
|
127
|
-
target: string;
|
|
128
|
-
kind: "not-null";
|
|
129
|
-
/** What is in the way, naming the obstacle rather than the rule. */
|
|
130
|
-
reason: string;
|
|
131
|
-
/** What would make it applicable. */
|
|
132
|
-
remedy: string;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* An admin that can plan a schema change.
|
|
137
|
-
*
|
|
138
|
-
* Planning only. Applying is `executeSql`, which every SQL admin already has,
|
|
139
|
-
* and committing belongs to whatever holds the repository — keeping those three
|
|
140
|
-
* apart is what lets the same plan be committed locally on a developer's machine
|
|
141
|
-
* and through a GitHub App from a cloud tenant.
|
|
142
|
-
*
|
|
143
|
-
* @group Admin
|
|
144
|
-
*/
|
|
145
|
-
export interface SchemaEditingAdmin {
|
|
146
|
-
/**
|
|
147
|
-
* Decide what the change means and render everything it needs.
|
|
148
|
-
*
|
|
149
|
-
* Rejects when the change is not applicable, carrying the classification so
|
|
150
|
-
* a caller can say which change was the problem.
|
|
151
|
-
*/
|
|
152
|
-
planSchemaChange(
|
|
153
|
-
before: unknown[],
|
|
154
|
-
after: unknown[],
|
|
155
|
-
options?: { paths?: Partial<SchemaCommitPaths> }
|
|
156
|
-
): Promise<SchemaChangePlan>;
|
|
157
|
-
}
|