@rebasepro/types 0.11.1-canary.gfd39654 → 0.12.0

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.
@@ -6,7 +6,7 @@ import type { EntityReference } from "../types/entities";
6
6
  */
7
7
  export type CollectionRegistryController<DB = Record<string, unknown>, EC extends CollectionConfig = CollectionConfig> = {
8
8
  /**
9
- * List of the mapped collections in the CMS.
9
+ * List of the mapped collections in the admin.
10
10
  * Each entry relates to a collection in the root database.
11
11
  * Each of the navigation entries in this field
12
12
  * generates an entry in the main menu.
@@ -108,12 +108,12 @@ export interface FindResponse<M extends Record<string, unknown> = Record<string,
108
108
  };
109
109
  }
110
110
  /**
111
- * Fluent query builder for the **admin CMS** — resolves to `FindResponse<M>`
111
+ * Fluent query builder for the **admin admin** — resolves to `FindResponse<M>`
112
112
  * (Snapshot-wrapped rows).
113
113
  *
114
114
  * @internal App developers should use {@link SDKQueryBuilderInterface}
115
115
  * (flat rows, returned by `client.data.*` / `context.data.*`). This
116
- * Snapshot-flavored variant backs the admin CMS internals only.
116
+ * Snapshot-flavored variant backs the admin admin internals only.
117
117
  *
118
118
  * @group Data
119
119
  */
@@ -129,13 +129,13 @@ export interface QueryBuilderInterface<M extends Record<string, unknown> = Recor
129
129
  listen(onUpdate: (data: FindResponse<M>) => void, onError?: (error: Error) => void): () => void;
130
130
  }
131
131
  /**
132
- * A single collection's CRUD accessor for the **admin CMS** — every method
132
+ * A single collection's CRUD accessor for the **admin admin** — every method
133
133
  * resolves to `Snapshot`-wrapped rows (`FindResponse<M>` / `Snapshot<M>`).
134
134
  *
135
135
  * @internal App developers do **not** use this. The public, symmetric surface
136
136
  * is {@link SDKCollectionClient} (flat rows), exposed as `client.data.products`
137
137
  * in the SDK and `context.data.products` in framework callbacks. This
138
- * Snapshot-flavored accessor backs the admin CMS view-model only.
138
+ * Snapshot-flavored accessor backs the admin admin view-model only.
139
139
  *
140
140
  * @group Data
141
141
  */
@@ -222,6 +222,86 @@ export interface FindResult<M extends Record<string, unknown> = Record<string, u
222
222
  /** Pagination metadata */
223
223
  meta: PaginationMeta;
224
224
  }
225
+ /**
226
+ * Which column an iteration seeks on, for keyset ("seek") pagination.
227
+ *
228
+ * Either the column name on its own — sorted ascending — or the column plus an
229
+ * explicit direction. The column must be **unique** and must be the column the
230
+ * query is ordered by; see {@link PageWalkOptions.cursor}.
231
+ *
232
+ * @group Data
233
+ */
234
+ export type CursorSpec<M extends Record<string, unknown> = Record<string, unknown>> = (Extract<keyof M, string>) | {
235
+ field: Extract<keyof M, string>;
236
+ direction?: "asc" | "desc";
237
+ };
238
+ /**
239
+ * How {@link SDKCollectionClient.iterate} / {@link SDKCollectionClient.findAll}
240
+ * walk a collection, layered on top of the normal `find()` parameters.
241
+ *
242
+ * @group Data
243
+ */
244
+ export interface PageWalkOptions<M extends Record<string, unknown> = Record<string, unknown>> {
245
+ /**
246
+ * Rows fetched per request. Defaults to 200; values below 1 are clamped up.
247
+ * This is the request size, not a result cap — the iteration keeps going
248
+ * until the server says there is nothing left.
249
+ */
250
+ pageSize?: number;
251
+ /**
252
+ * Paginate by **seeking on a column** instead of by offset.
253
+ *
254
+ * Offset paging — the default — re-counts rows on every request, so a row
255
+ * inserted or deleted *while the iteration runs* shifts the window and the
256
+ * walk silently skips or repeats rows. Seeking is immune to that: each page
257
+ * asks for rows strictly after the last one seen, so concurrent writes
258
+ * before the cursor cannot move it.
259
+ *
260
+ * Prefer this whenever the collection has a unique, sortable column
261
+ * (typically its primary key). The column must be unique — a repeated value
262
+ * at a page boundary either skips rows or stalls, and the iterator throws
263
+ * rather than looping — and the query is ordered by it, so a `cursor` and a
264
+ * conflicting `orderBy` is an error, not a silent override.
265
+ *
266
+ * Implemented with the parameters `find()` already takes (an `orderBy` plus
267
+ * a `>` / `<` filter on the cursor column), so it works on every transport
268
+ * and needs nothing new from the server.
269
+ *
270
+ * @example
271
+ * for await (const job of client.data.jobs.iterate({ cursor: "id" })) { … }
272
+ */
273
+ cursor?: CursorSpec<M>;
274
+ /**
275
+ * Hard ceiling on the number of requests one walk may make, so a server
276
+ * that never stops saying `hasMore` cannot spin forever. Defaults to
277
+ * 10 000 pages; hitting it throws.
278
+ */
279
+ maxPages?: number;
280
+ }
281
+ /**
282
+ * Parameters accepted by {@link SDKCollectionClient.iterate} — everything
283
+ * `find()` takes except the window itself (`limit`, `offset`, `page`), which
284
+ * the iterator owns, plus the walk options.
285
+ *
286
+ * @group Data
287
+ */
288
+ export type IterateParams<M extends Record<string, unknown> = Record<string, unknown>> = Omit<FindParams<M>, "limit" | "offset" | "page"> & PageWalkOptions<M>;
289
+ /**
290
+ * Parameters accepted by {@link SDKCollectionClient.findAll}: the iteration
291
+ * parameters plus the ceiling that keeps a whole collection from being pulled
292
+ * into memory unnoticed.
293
+ *
294
+ * @group Data
295
+ */
296
+ export type FindAllParams<M extends Record<string, unknown> = Record<string, unknown>> = IterateParams<M> & {
297
+ /**
298
+ * Most rows to materialise. Defaults to 10 000. Exceeding it **throws**
299
+ * — a truncated array returned as if it were the whole answer is the
300
+ * kind of quiet wrong that shows up months later in a report. Pass
301
+ * `Infinity` to opt out deliberately, or use `iterate()` to stream.
302
+ */
303
+ maxRows?: number;
304
+ };
225
305
  /**
226
306
  * Fluent Query Builder Interface for the SDK client.
227
307
  * Returns `FindResult<M>` (flat rows) instead of `FindResponse<M>` (Entity-wrapped).
@@ -244,7 +324,7 @@ export interface SDKQueryBuilderInterface<M extends Record<string, unknown> = Re
244
324
  * SDK collection client — returns flat rows, no Entity wrapper.
245
325
  *
246
326
  * This is the public API surface for app developers using
247
- * `createRebaseClient()`. CMS internals use `CollectionAccessor` instead.
327
+ * `createRebaseClient()`. admin internals use `CollectionAccessor` instead.
248
328
  *
249
329
  * Type parameters:
250
330
  * - `M` — the **Row** shape returned by reads (`find`, `findById`, `listen`).
@@ -290,6 +370,65 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
290
370
  * Find multiple records with optional filtering, pagination, and sorting.
291
371
  */
292
372
  find(params?: FindParams<M>): Promise<FindResult<M>>;
373
+ /**
374
+ * Walk every record matching a query, one row at a time, fetching pages as
375
+ * the consumer consumes them.
376
+ *
377
+ * This is the pagination primitive: `find()` returns one window, `iterate()`
378
+ * returns all of them without the caller hand-rolling the
379
+ * `limit` / `offset += ` / "am I done yet" loop. Nothing is buffered — rows
380
+ * are yielded as each page arrives, so a million-row walk costs one page of
381
+ * memory. `break` stops the walk and no further requests are made.
382
+ *
383
+ * Termination is driven by the server's `meta.hasMore`, never by comparing
384
+ * a page's length against the requested limit — a final page that happens
385
+ * to be exactly full is indistinguishable that way, and a walk that stops
386
+ * there drops rows. An empty page also ends the walk, and
387
+ * {@link PageWalkOptions.maxPages} bounds a server that never stops saying
388
+ * there is more.
389
+ *
390
+ * ## Consistency
391
+ *
392
+ * By default this pages by **offset**, which is only as stable as the table
393
+ * is still: a row inserted or deleted ahead of the cursor between two
394
+ * requests shifts every later window, so the walk can skip a row or hand
395
+ * back the same one twice. That is inherent to offset paging, not a bug
396
+ * here. On a collection with a unique sortable column, pass
397
+ * {@link PageWalkOptions.cursor} to seek on it instead — the walk then
398
+ * asks for rows strictly after the last one it saw, which concurrent writes
399
+ * cannot perturb.
400
+ *
401
+ * @example
402
+ * for await (const job of client.data.jobs.iterate({
403
+ * where: { status: ["==", "queued"] },
404
+ * cursor: "id",
405
+ * pageSize: 500
406
+ * })) {
407
+ * await handle(job);
408
+ * }
409
+ */
410
+ iterate(params?: IterateParams<M>): AsyncIterableIterator<M>;
411
+ /**
412
+ * {@link iterate}, collected into an array.
413
+ *
414
+ * Convenient when the result is known to be small and awkward to stream.
415
+ * Because "known to be small" is an assumption and not a fact, the result is
416
+ * capped — 10 000 rows by default — and going over the cap **throws**
417
+ * rather than returning a short array that reads like a complete one. Raise
418
+ * {@link FindAllParams.maxRows} when the data really is bigger, or switch to
419
+ * `iterate()` and stream it.
420
+ *
421
+ * The offset-drift caveat on {@link iterate} applies here too.
422
+ *
423
+ * @throws When more rows match than `maxRows` allows.
424
+ *
425
+ * @example
426
+ * const overdue = await client.data.invoices.findAll({
427
+ * where: { due_at: ["<", today] },
428
+ * cursor: "id"
429
+ * });
430
+ */
431
+ findAll(params?: FindAllParams<M>): Promise<M[]>;
293
432
  /**
294
433
  * Find a single record by its ID.
295
434
  */
@@ -367,16 +506,16 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
367
506
  include(...relations: string[]): SDKQueryBuilderInterface<M>;
368
507
  }
369
508
  /**
370
- * The unified data access object for the **admin CMS** (Entity-shaped).
509
+ * The unified data access object for the **admin admin** (Entity-shaped).
371
510
  *
372
511
  * Access collections as dynamic properties: `data.products.find(...)`. Each
373
512
  * accessor returns `Entity`-wrapped records (`{ id, path, values }`) — the
374
- * view-model the CMS renders. This is what `useData()` / the admin
513
+ * view-model the admin renders. This is what `useData()` / the admin
375
514
  * `RebaseContext.data` are backed by.
376
515
  *
377
516
  * @internal App developers do **not** use this — they use
378
517
  * {@link RebaseSdkData} (flat rows), which is what the SDK client and backend
379
- * `context.data` expose. This Entity-shaped map backs the admin CMS only.
518
+ * `context.data` expose. This Entity-shaped map backs the admin admin only.
380
519
  *
381
520
  * @group Data
382
521
  */
@@ -415,7 +554,7 @@ export type RebaseData<DB = unknown> = {
415
554
  *
416
555
  * Every accessor returns flat rows (the table's columns) via
417
556
  * {@link SDKCollectionClient} — access fields directly (`row.title`), never
418
- * `row.values.title`. The admin CMS uses {@link RebaseData} (Entity) instead.
557
+ * `row.values.title`. The admin uses {@link RebaseData} (Entity) instead.
419
558
  *
420
559
  * @example
421
560
  * // Frontend SDK
package/dist/index.es.js CHANGED
@@ -336,6 +336,168 @@ var ADMIN_COLLECTION_KEYS = [
336
336
  "sort",
337
337
  "titleProperty"
338
338
  ];
339
+ /**
340
+ * Every key that belongs inside a *property's* `admin` block, as data.
341
+ *
342
+ * The union of `AdminPropertyOptions` and its per-type extensions
343
+ * (`AdminStringOptions`, `AdminArrayOptions`, …) in `@rebasepro/admin-types`.
344
+ * It lives here for the same reason {@link ADMIN_COLLECTION_KEYS} does: the
345
+ * runtime consumers are core packages that the BaaS guard forbids from
346
+ * importing `@rebasepro/admin-types`. Here it is the boot-time collection
347
+ * validator in `@rebasepro/server`, which has to tell "you left `readOnly` at
348
+ * the top of the property, where nothing reads it" apart from "you invented a
349
+ * key we have never heard of".
350
+ *
351
+ * `@rebasepro/admin-types` re-exports this and asserts it names only real
352
+ * option keys.
353
+ *
354
+ * @group Models
355
+ */
356
+ var ADMIN_PROPERTY_KEYS = [
357
+ "canAddElements",
358
+ "clearable",
359
+ "columnWidth",
360
+ "customProps",
361
+ "disabled",
362
+ "expanded",
363
+ "Field",
364
+ "Filter",
365
+ "filterOperators",
366
+ "fixedFilter",
367
+ "hideFromCollection",
368
+ "includeEntityLink",
369
+ "includeId",
370
+ "markdown",
371
+ "minimalistView",
372
+ "multiline",
373
+ "Preview",
374
+ "previewAsTag",
375
+ "previewProperties",
376
+ "readOnly",
377
+ "sortable",
378
+ "spreadChildren",
379
+ "urlPreview",
380
+ "widget",
381
+ "widthPercentage"
382
+ ];
383
+ //#endregion
384
+ //#region src/types/data_source.ts
385
+ /**
386
+ * The default data-source key, used when a collection does not name a
387
+ * `dataSource`. Shared by the frontend router and the backend driver
388
+ * registry so both agree on "the default database".
389
+ * @group Models
390
+ */
391
+ var DEFAULT_DATA_SOURCE_KEY = "(default)";
392
+ /**
393
+ * Relation kinds assumed filterable when a driver does not say.
394
+ *
395
+ * `belongsTo` alone: its filter is a comparison on a column of the row being
396
+ * filtered, the one shape that needs no query construction a driver might not
397
+ * have. Everything else is a correlated subquery over another table.
398
+ *
399
+ * @group Models
400
+ */
401
+ var DEFAULT_FILTERABLE_RELATION_KINDS = ["belongsTo"];
402
+ /** @group Models */
403
+ var POSTGRES_CAPABILITIES = {
404
+ key: "postgres",
405
+ label: "PostgreSQL",
406
+ supportsRelations: true,
407
+ supportsSubcollections: false,
408
+ supportsRLS: true,
409
+ supportsReferences: false,
410
+ supportsColumnTypes: true,
411
+ supportsRealtime: true,
412
+ supportsVectors: true,
413
+ filterOperators: ALL_WHERE_FILTER_OPS,
414
+ filterableRelationKinds: [
415
+ "belongsTo",
416
+ "manyToMany",
417
+ "hasMany",
418
+ "hasOne"
419
+ ],
420
+ supportsSQLAdmin: true,
421
+ supportsDocumentAdmin: false,
422
+ supportsSchemaAdmin: true
423
+ };
424
+ /** @group Models */
425
+ var FIREBASE_CAPABILITIES = {
426
+ key: "firestore",
427
+ label: "Firebase / Firestore",
428
+ supportsRelations: false,
429
+ supportsSubcollections: true,
430
+ supportsRLS: false,
431
+ supportsReferences: true,
432
+ supportsColumnTypes: false,
433
+ supportsRealtime: true,
434
+ supportsVectors: false,
435
+ filterOperators: ALL_WHERE_FILTER_OPS.filter((op) => op !== "like" && op !== "ilike" && op !== "not-like" && op !== "not-ilike"),
436
+ filterableRelationKinds: [],
437
+ supportsSQLAdmin: false,
438
+ supportsDocumentAdmin: false,
439
+ supportsSchemaAdmin: false
440
+ };
441
+ /** @group Models */
442
+ var MONGODB_CAPABILITIES = {
443
+ key: "mongodb",
444
+ label: "MongoDB",
445
+ supportsRelations: false,
446
+ supportsSubcollections: true,
447
+ supportsRLS: false,
448
+ supportsReferences: true,
449
+ supportsColumnTypes: false,
450
+ supportsRealtime: false,
451
+ supportsVectors: false,
452
+ filterOperators: ALL_WHERE_FILTER_OPS,
453
+ filterableRelationKinds: [],
454
+ supportsSQLAdmin: false,
455
+ supportsDocumentAdmin: true,
456
+ supportsSchemaAdmin: true
457
+ };
458
+ /**
459
+ * Fallback capabilities when the driver is unknown.
460
+ * Enables everything so nothing is hidden unexpectedly.
461
+ * @group Models
462
+ */
463
+ var DEFAULT_CAPABILITIES = {
464
+ key: "(default)",
465
+ label: "Default",
466
+ supportsRelations: true,
467
+ supportsSubcollections: true,
468
+ supportsRLS: true,
469
+ supportsReferences: true,
470
+ supportsColumnTypes: true,
471
+ supportsRealtime: true,
472
+ supportsVectors: true,
473
+ filterOperators: ALL_WHERE_FILTER_OPS,
474
+ filterableRelationKinds: DEFAULT_FILTERABLE_RELATION_KINDS,
475
+ supportsSQLAdmin: true,
476
+ supportsDocumentAdmin: true,
477
+ supportsSchemaAdmin: true
478
+ };
479
+ var CAPABILITIES_REGISTRY = {
480
+ postgres: POSTGRES_CAPABILITIES,
481
+ firestore: FIREBASE_CAPABILITIES,
482
+ mongodb: MONGODB_CAPABILITIES,
483
+ "(default)": DEFAULT_CAPABILITIES
484
+ };
485
+ /**
486
+ * Look up capabilities for a given engine key.
487
+ * If `engine` is undefined or not found, returns `DEFAULT_CAPABILITIES`.
488
+ * @group Models
489
+ */
490
+ function getDataSourceCapabilities(engine) {
491
+ if (!engine) return POSTGRES_CAPABILITIES;
492
+ return CAPABILITIES_REGISTRY[engine] ?? DEFAULT_CAPABILITIES;
493
+ }
494
+ /**
495
+ * Register custom capabilities for a third-party driver.
496
+ * @group Models
497
+ */
498
+ function registerDataSourceCapabilities(capabilities) {
499
+ CAPABILITIES_REGISTRY[capabilities.key] = capabilities;
500
+ }
339
501
  //#endregion
340
502
  //#region src/types/collections.ts
341
503
  /**
@@ -354,6 +516,29 @@ function isPostgresCollectionConfig(collection) {
354
516
  return !collection.engine || collection.engine === "postgres";
355
517
  }
356
518
  /**
519
+ * Narrows to the SQL collection fields — `table`, `relations`,
520
+ * `disableDefaultPolicies` — by asking the engine's declared capabilities
521
+ * rather than by naming Postgres.
522
+ *
523
+ * The two halves of this already existed and were never joined. The engine
524
+ * split (`PostgresCollectionConfig` / `FirebaseCollectionConfig` /
525
+ * `MongoDBCollectionConfig`) said which fields belong to which engine at the
526
+ * type level; {@link DataSourceCapabilities} said the same thing at runtime,
527
+ * down to a `supportsRelations` flag. So call sites guarded on the capability
528
+ * and then read a field the base type had to declare for them — which is why
529
+ * those fields were on the base, and why a MongoDB collection could be written
530
+ * with a `table`.
531
+ *
532
+ * Prefer this over {@link isPostgresCollectionConfig} wherever the question is
533
+ * "does this collection live in a SQL table", so a custom SQL engine
534
+ * registered through `registerDataSourceCapabilities` is included.
535
+ *
536
+ * @group Models
537
+ */
538
+ function isRelationalCollectionConfig(collection) {
539
+ return getDataSourceCapabilities(collection.engine).supportsRelations;
540
+ }
541
+ /**
357
542
  * Type guard for Firebase / Firestore collections.
358
543
  * @group Models
359
544
  */
@@ -523,101 +708,6 @@ function isChannelBusInstance(setting) {
523
708
  return typeof setting?.publish === "function";
524
709
  }
525
710
  //#endregion
526
- //#region src/types/data_source.ts
527
- /**
528
- * The default data-source key, used when a collection does not name a
529
- * `dataSource`. Shared by the frontend router and the backend driver
530
- * registry so both agree on "the default database".
531
- * @group Models
532
- */
533
- var DEFAULT_DATA_SOURCE_KEY = "(default)";
534
- /** @group Models */
535
- var POSTGRES_CAPABILITIES = {
536
- key: "postgres",
537
- label: "PostgreSQL",
538
- supportsRelations: true,
539
- supportsSubcollections: false,
540
- supportsRLS: true,
541
- supportsReferences: false,
542
- supportsColumnTypes: true,
543
- supportsRealtime: true,
544
- filterOperators: ALL_WHERE_FILTER_OPS,
545
- supportsSQLAdmin: true,
546
- supportsDocumentAdmin: false,
547
- supportsSchemaAdmin: true
548
- };
549
- /** @group Models */
550
- var FIREBASE_CAPABILITIES = {
551
- key: "firestore",
552
- label: "Firebase / Firestore",
553
- supportsRelations: false,
554
- supportsSubcollections: true,
555
- supportsRLS: false,
556
- supportsReferences: true,
557
- supportsColumnTypes: false,
558
- supportsRealtime: true,
559
- filterOperators: ALL_WHERE_FILTER_OPS.filter((op) => op !== "like" && op !== "ilike" && op !== "not-like" && op !== "not-ilike"),
560
- supportsSQLAdmin: false,
561
- supportsDocumentAdmin: false,
562
- supportsSchemaAdmin: false
563
- };
564
- /** @group Models */
565
- var MONGODB_CAPABILITIES = {
566
- key: "mongodb",
567
- label: "MongoDB",
568
- supportsRelations: false,
569
- supportsSubcollections: true,
570
- supportsRLS: false,
571
- supportsReferences: true,
572
- supportsColumnTypes: false,
573
- supportsRealtime: false,
574
- filterOperators: ALL_WHERE_FILTER_OPS,
575
- supportsSQLAdmin: false,
576
- supportsDocumentAdmin: true,
577
- supportsSchemaAdmin: true
578
- };
579
- /**
580
- * Fallback capabilities when the driver is unknown.
581
- * Enables everything so nothing is hidden unexpectedly.
582
- * @group Models
583
- */
584
- var DEFAULT_CAPABILITIES = {
585
- key: "(default)",
586
- label: "Default",
587
- supportsRelations: true,
588
- supportsSubcollections: true,
589
- supportsRLS: true,
590
- supportsReferences: true,
591
- supportsColumnTypes: true,
592
- supportsRealtime: true,
593
- filterOperators: ALL_WHERE_FILTER_OPS,
594
- supportsSQLAdmin: true,
595
- supportsDocumentAdmin: true,
596
- supportsSchemaAdmin: true
597
- };
598
- var CAPABILITIES_REGISTRY = {
599
- postgres: POSTGRES_CAPABILITIES,
600
- firestore: FIREBASE_CAPABILITIES,
601
- mongodb: MONGODB_CAPABILITIES,
602
- "(default)": DEFAULT_CAPABILITIES
603
- };
604
- /**
605
- * Look up capabilities for a given engine key.
606
- * If `engine` is undefined or not found, returns `DEFAULT_CAPABILITIES`.
607
- * @group Models
608
- */
609
- function getDataSourceCapabilities(engine) {
610
- if (!engine) return POSTGRES_CAPABILITIES;
611
- return CAPABILITIES_REGISTRY[engine] ?? DEFAULT_CAPABILITIES;
612
- }
613
- /**
614
- * Register custom capabilities for a third-party driver.
615
- * @group Models
616
- */
617
- function registerDataSourceCapabilities(capabilities) {
618
- CAPABILITIES_REGISTRY[capabilities.key] = capabilities;
619
- }
620
- //#endregion
621
711
  //#region src/types/storage_source.ts
622
712
  /**
623
713
  * Describes a named storage backend — a place files live.
@@ -638,6 +728,98 @@ function registerDataSourceCapabilities(capabilities) {
638
728
  * @group Models
639
729
  */
640
730
  var DEFAULT_STORAGE_SOURCE_KEY = "(default)";
731
+ /**
732
+ * The environment-variable suffix for a storage or data source key.
733
+ *
734
+ * `""` for the default source — so a single-bucket project keeps configuring
735
+ * plain `S3_BUCKET` — and `__<KEY>` for every named one, uppercased with
736
+ * non-alphanumerics collapsed to underscores: `media-cdn` → `S3_BUCKET__MEDIA_CDN`.
737
+ *
738
+ * The rule derives the variable name from the declared key rather than
739
+ * discovering keys by scanning the environment. Scanning would have to guess how
740
+ * `S3_BUCKET__MEDIA_CDN` splits into a key; deriving cannot be ambiguous, and a
741
+ * typo surfaces as a missing source at boot instead of a silently ignored
742
+ * variable.
743
+ *
744
+ * It lives in this package, with no dependencies, because four things must agree
745
+ * on it exactly: the CLI (validating a build), the runtime (reading its own
746
+ * environment), the control plane (writing a tenant's Secret), and the docs. A
747
+ * second implementation of a naming convention is a second chance to disagree.
748
+ *
749
+ * @group Models
750
+ */
751
+ function storageEnvSuffix(key, defaultKey = DEFAULT_STORAGE_SOURCE_KEY) {
752
+ if (!key || key === defaultKey) return "";
753
+ const normalized = key.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
754
+ 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.`);
755
+ return `__${normalized}`;
756
+ }
757
+ /**
758
+ * Two distinct keys that collapse onto the same variable name, or `null`.
759
+ *
760
+ * `media-cdn` and `media_cdn` are different source keys but the same suffix, so
761
+ * without this one of them silently reads the other's configuration. Returns the
762
+ * offending pair rather than throwing, so each caller can raise it in its own
763
+ * idiom — a `BundleError` at boot, a build failure in the CLI, a rejected deploy
764
+ * in a control plane.
765
+ *
766
+ * @group Models
767
+ */
768
+ function findStorageSuffixCollision(keys, defaultKey = DEFAULT_STORAGE_SOURCE_KEY) {
769
+ const seen = /* @__PURE__ */ new Map();
770
+ for (const key of keys) {
771
+ const suffix = storageEnvSuffix(key, defaultKey);
772
+ const existing = seen.get(suffix);
773
+ if (existing !== void 0 && existing !== key) return {
774
+ a: existing,
775
+ b: key,
776
+ suffix
777
+ };
778
+ seen.set(suffix, key);
779
+ }
780
+ return null;
781
+ }
782
+ /**
783
+ * Merge the two places a project may declare storage sources into one list.
784
+ *
785
+ * `rebase.json` is authoritative for every field it states. Config code may add
786
+ * sources it does not mention and fill in fields it left out, but may not
787
+ * contradict it: the manifest is what a host reads to decide which buckets need
788
+ * configuring, and a runtime that quietly disagreed with it would put the
789
+ * console back to describing a topology the tenant does not have — the exact
790
+ * failure this whole mechanism exists to end.
791
+ *
792
+ * Note what is *not* here: no default source is invented when both inputs are
793
+ * empty. That decision belongs to the resolver, which knows whether declaring
794
+ * nothing means "one plain bucket" (it does) or "no storage at all".
795
+ *
796
+ * @group Models
797
+ */
798
+ function normalizeStorageSources(declared, exported) {
799
+ const merged = /* @__PURE__ */ new Map();
800
+ const declaredEntries = Array.isArray(declared) ? declared.filter((d) => d?.key).map((d) => [d.key, d]) : Object.entries(declared ?? {});
801
+ for (const [key, config] of declaredEntries) merged.set(key, {
802
+ key,
803
+ engine: config.engine,
804
+ transport: config.transport ?? "server",
805
+ ...config.label !== void 0 ? { label: config.label } : {}
806
+ });
807
+ for (const definition of exported ?? []) {
808
+ if (!definition?.key) continue;
809
+ const existing = merged.get(definition.key);
810
+ if (!existing) {
811
+ merged.set(definition.key, {
812
+ key: definition.key,
813
+ engine: definition.engine,
814
+ transport: definition.transport ?? "server",
815
+ ...definition.label !== void 0 ? { label: definition.label } : {}
816
+ });
817
+ continue;
818
+ }
819
+ if (existing.label === void 0 && definition.label !== void 0) existing.label = definition.label;
820
+ }
821
+ return Array.from(merged.values());
822
+ }
641
823
  //#endregion
642
824
  //#region src/types/component_ref.ts
643
825
  /**
@@ -656,8 +838,16 @@ function isLazyComponentRef(ref) {
656
838
  * not read. A runtime accepts any bundle whose `bundleFormat` is less than or
657
839
  * equal to its own — old bundles keep booting on new runtimes, which is the
658
840
  * whole point of separating the artifact from the engine.
841
+ *
842
+ * - **1** — `mode: "cms" | "baas" | "static"`, `entry.static` a single directory
843
+ * string, `entry.admin` for a bundled admin panel.
844
+ * - **2** — `kind: "backend" | "static"`, `entry.static` a list of
845
+ * {@link RebaseBundleStatic}, `entry.admin` removed. A format-1 runtime reading
846
+ * one of these would find no `mode` and an array where it expects a string, so
847
+ * the bump is what turns that into a refusal to boot instead of a bundle that
848
+ * starts and serves nothing.
659
849
  */
660
- var BUNDLE_FORMAT_VERSION = 1;
850
+ var BUNDLE_FORMAT_VERSION = 2;
661
851
  /**
662
852
  * The runtime contract major.
663
853
  *
@@ -955,6 +1145,6 @@ function isPublicStoragePath(path) {
955
1145
  return p.startsWith("public/") || p.startsWith(`default/public/`);
956
1146
  }
957
1147
  //#endregion
958
- export { ADMIN_COLLECTION_KEYS, ALL_WHERE_FILTER_OPS, ANONYMOUS_USER_ID, BUNDLE_FORMAT_VERSION, CANONICAL_TO_REST, DEFAULT_CAPABILITIES, DEFAULT_DATA_SOURCE_KEY, DEFAULT_LIST_LIMIT, DEFAULT_STORAGE_SOURCE_KEY, DEFAULT_VECTOR_LIST_LIMIT, EntityReference, EntityRelation, FIREBASE_CAPABILITIES, GeoPoint, MAX_LIST_LIMIT, MONGODB_CAPABILITIES, NULL_OPS, POSTGRES_CAPABILITIES, PUBLIC_STORAGE_PREFIX, REST_TO_CANONICAL, RUNTIME_CONTRACT_VERSION, RebaseApiError, RebaseClientError, SCHEMA_VERSION_HEADER, Vector, canonicalSchemaPayload, computeSchemaVersion, deserializeCollections, getCollectionDataPath, getDataSourceCapabilities, getDeclaredSubcollections, hasForeignKeyOnTarget, isBranchAdmin, isChannelBusInstance, isDocumentAdmin, isFirebaseCollectionConfig, isLazyComponentRef, isManyToMany, isMongoDBCollectionConfig, isPostgresCollectionConfig, isPublicStoragePath, isSQLAdmin, isSchemaAdmin, isSerializedCollectionRef, isToMany, policy, registerDataSourceCapabilities, resolveClientListLimit, serializeCollections, toCanonicalOp };
1148
+ export { ADMIN_COLLECTION_KEYS, ADMIN_PROPERTY_KEYS, ALL_WHERE_FILTER_OPS, ANONYMOUS_USER_ID, BUNDLE_FORMAT_VERSION, CANONICAL_TO_REST, DEFAULT_CAPABILITIES, DEFAULT_DATA_SOURCE_KEY, DEFAULT_FILTERABLE_RELATION_KINDS, DEFAULT_LIST_LIMIT, DEFAULT_STORAGE_SOURCE_KEY, DEFAULT_VECTOR_LIST_LIMIT, EntityReference, EntityRelation, FIREBASE_CAPABILITIES, GeoPoint, MAX_LIST_LIMIT, MONGODB_CAPABILITIES, NULL_OPS, POSTGRES_CAPABILITIES, PUBLIC_STORAGE_PREFIX, REST_TO_CANONICAL, RUNTIME_CONTRACT_VERSION, RebaseApiError, RebaseClientError, SCHEMA_VERSION_HEADER, Vector, canonicalSchemaPayload, computeSchemaVersion, deserializeCollections, findStorageSuffixCollision, getCollectionDataPath, getDataSourceCapabilities, getDeclaredSubcollections, hasForeignKeyOnTarget, isBranchAdmin, isChannelBusInstance, isDocumentAdmin, isFirebaseCollectionConfig, isLazyComponentRef, isManyToMany, isMongoDBCollectionConfig, isPostgresCollectionConfig, isPublicStoragePath, isRelationalCollectionConfig, isSQLAdmin, isSchemaAdmin, isSerializedCollectionRef, isToMany, normalizeStorageSources, policy, registerDataSourceCapabilities, resolveClientListLimit, serializeCollections, storageEnvSuffix, toCanonicalOp };
959
1149
 
960
1150
  //# sourceMappingURL=index.es.js.map