@rebasepro/types 0.16.1-canary.gef08a6e → 0.17.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.
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * There is no *type* for the block in this package any more, and that is the point:
5
5
  * `admin` is not declared on `BaseCollectionConfig` or on any property here, so a
6
- * BaaS install cannot even write one. `@rebasepro/admin-types` adds the field back by
6
+ * BaaS install cannot even write one. `@rebasepro/cms-types` adds the field back by
7
7
  * declaration merging, which is why installing it is what makes the admin surface
8
8
  * appear.
9
9
  *
@@ -14,7 +14,7 @@
14
14
  * Every key that belongs inside a collection's `admin` block, as data.
15
15
  *
16
16
  * The type that describes these fields is `AdminCollectionOptions` in
17
- * `@rebasepro/admin-types`, and it is erased at build time — but three runtime
17
+ * `@rebasepro/cms-types`, and it is erased at build time — but three runtime
18
18
  * consumers need the list, and two of them are core:
19
19
  *
20
20
  * - `serializeCollections`, to drop the block from the contract
@@ -24,7 +24,7 @@
24
24
  * backend ignores it and the panel never finds it again.
25
25
  * - the `collections-admin-block` codemod
26
26
  *
27
- * `@rebasepro/admin-types` re-exports this and asserts it names only real option
27
+ * `@rebasepro/cms-types` re-exports this and asserts it names only real option
28
28
  * keys; the count is pinned by a test there.
29
29
  *
30
30
  * @group Models
@@ -36,15 +36,15 @@ export type AdminCollectionKey = typeof ADMIN_COLLECTION_KEYS[number];
36
36
  * Every key that belongs inside a *property's* `admin` block, as data.
37
37
  *
38
38
  * The union of `AdminPropertyOptions` and its per-type extensions
39
- * (`AdminStringOptions`, `AdminArrayOptions`, …) in `@rebasepro/admin-types`.
39
+ * (`AdminStringOptions`, `AdminArrayOptions`, …) in `@rebasepro/cms-types`.
40
40
  * It lives here for the same reason {@link ADMIN_COLLECTION_KEYS} does: the
41
41
  * runtime consumers are core packages that the BaaS guard forbids from
42
- * importing `@rebasepro/admin-types`. Here it is the boot-time collection
42
+ * importing `@rebasepro/cms-types`. Here it is the boot-time collection
43
43
  * validator in `@rebasepro/server`, which has to tell "you left `readOnly` at
44
44
  * the top of the property, where nothing reads it" apart from "you invented a
45
45
  * key we have never heard of".
46
46
  *
47
- * `@rebasepro/admin-types` re-exports this and asserts it names only real
47
+ * `@rebasepro/cms-types` re-exports this and asserts it names only real
48
48
  * option keys.
49
49
  *
50
50
  * @group Models
@@ -65,7 +65,7 @@ export type AdminPropertyKey = typeof ADMIN_PROPERTY_KEYS[number];
65
65
  * in favour of the value the user changed away from.
66
66
  *
67
67
  * This lives here, next to the key lists, because it had two implementations —
68
- * `toAdminCollectionConfig` in `@rebasepro/admin-types` and `nestAdminKeys` in
68
+ * `toAdminCollectionConfig` in `@rebasepro/cms-types` and `nestAdminKeys` in
69
69
  * `@rebasepro/server`'s schema editor — that agreed on everything except that
70
70
  * precedence, which is the only part that decides whether a save is visible.
71
71
  *
@@ -5,6 +5,7 @@ import type { EmailSendResult } from "../controllers/email.js";
5
5
  import type { Relation } from "./relations.js";
6
6
  import type { SecurityRule } from "./security_rules.js";
7
7
  import type { SearchConfig } from "./search.js";
8
+ import type { CollectionIndex } from "./indexes.js";
8
9
  /**
9
10
  * Base interface containing all driver-agnostic collection properties.
10
11
  * Use {@link PostgresCollectionConfig} or {@link FirebaseCollectionConfig} for
@@ -248,6 +249,23 @@ export interface PostgresCollectionConfig<M extends Record<string, unknown> = Re
248
249
  * @see SearchConfig
249
250
  */
250
251
  search?: SearchConfig;
252
+ /**
253
+ * Ordinary indexes on this collection's table.
254
+ *
255
+ * Collection-level, not per-property, because an index over two columns
256
+ * has no single property to hang on and a partial index has none at all —
257
+ * and because a second declaration site for the single-column case would
258
+ * put the same object in two places. An index's identity is a column list
259
+ * in an order; the single-column case is a degenerate one, not a special
260
+ * one.
261
+ *
262
+ * `VectorProperty.index` stays where it is: an ANN structure is a property
263
+ * of the column's type, not of a query.
264
+ *
265
+ * Postgres-only, like {@link SearchConfig}: refused on another engine
266
+ * rather than silently ignored.
267
+ */
268
+ indexes?: readonly CollectionIndex<Extract<keyof M, string>>[];
251
269
  }
252
270
  /**
253
271
  * A collection backed by Firebase / Firestore.
@@ -537,7 +555,7 @@ export interface AuthCollectionConfig {
537
555
  *
538
556
  * Set to `false` to disable, or pass a custom `EntityAction` to replace the UI.
539
557
  *
540
- * The object form is an `EntityAction` from `@rebasepro/admin-types`, typed
558
+ * The object form is an `EntityAction` from `@rebasepro/cms-types`, typed
541
559
  * here as `object` because it is a React component with admin controllers in
542
560
  * its props and nothing on the server reads it — only whether the built-in
543
561
  * action is injected, which is the boolean.
@@ -18,7 +18,7 @@
18
18
  *
19
19
  * The cost is that the return type is `unknown` rather than `ReactNode`, so a
20
20
  * function that returns something React could not render is accepted here.
21
- * `@rebasepro/admin-types` re-exports a `ReactComponentRef<P>` narrowed against
21
+ * `@rebasepro/cms-types` re-exports a `ReactComponentRef<P>` narrowed against
22
22
  * the real `React.ComponentType` for authoring and for the admin's internals,
23
23
  * which restores that check where it can be enforced.
24
24
  */
@@ -5,6 +5,7 @@ export * from "./properties.js";
5
5
  export * from "./admin_block.js";
6
6
  export * from "./collections.js";
7
7
  export * from "./search.js";
8
+ export * from "./indexes.js";
8
9
  export * from "./relations.js";
9
10
  export * from "./policy.js";
10
11
  export * from "./rls-functions.js";
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Ordinary indexes, declared on a collection.
3
+ *
4
+ * Distinct from the two index-shaped things Rebase already builds. A `search`
5
+ * block builds a GIN index over a generated `tsvector`, and a `vector`
6
+ * property builds an ANN index over an embedding; both are structures the
7
+ * *feature* owns and neither is a query the developer wrote. This is the plain
8
+ * case — the btree behind a `where` clause — which had no declaration site at
9
+ * all, so the only way to have one was to write it by hand, where the next
10
+ * `rebase db push` planned it away.
11
+ *
12
+ * Every form here is core Postgres, deliberately. See {@link CollectionIndex}.
13
+ */
14
+ /**
15
+ * A key column of an index whose access method has no ordering.
16
+ *
17
+ * `gin` and `brin` reject `ASC`/`DESC`/`NULLS` outright — Postgres answers
18
+ * `access method "gin" does not support ASC/DESC options` — so those methods
19
+ * take this narrower shape and the combination is unrepresentable rather than
20
+ * refused at build time.
21
+ */
22
+ export interface UnorderedIndexKey<Keys extends string = string> {
23
+ /**
24
+ * A property key on this collection — never a column name.
25
+ *
26
+ * Which column that resolves to depends on the property, and the two
27
+ * differ in exactly the case an index is most often wanted for: a
28
+ * `belongsTo` relation compiles to its resolved `localKey`
29
+ * (`primaryCategory` → `primary_category_id`), not to the snake-cased
30
+ * property key. Anything else resolves through `columnName`, or the
31
+ * snake-case default when it declares none.
32
+ *
33
+ * Writing the column name here would work for most properties and quietly
34
+ * index nothing for a foreign key, which is the one people reach for.
35
+ */
36
+ prop: Keys | (string & {});
37
+ }
38
+ /**
39
+ * A key column of an index, when its order matters.
40
+ *
41
+ * `direction` and `nulls` earn their place only when a query's `ORDER BY`
42
+ * mixes directions. A lone `DESC` index is redundant with its `ASC` twin —
43
+ * Postgres scans a btree backwards just as fast — and declaring both is
44
+ * refused.
45
+ *
46
+ * Writing the Postgres default down explicitly is free: the derived name
47
+ * hashes the *effective* order, so adding `direction: "asc"` to a column that
48
+ * was already ascending is not a redefinition and rebuilds nothing.
49
+ */
50
+ export interface IndexKey<Keys extends string = string> extends UnorderedIndexKey<Keys> {
51
+ direction?: "asc" | "desc";
52
+ /** Postgres's own default: `last` under `asc`, `first` under `desc`. */
53
+ nulls?: "first" | "last";
54
+ }
55
+ /**
56
+ * The rows a partial index covers.
57
+ *
58
+ * Structure rather than a SQL string, and this is the most load-bearing choice
59
+ * in the type. A string would be replayed verbatim by Atlas in a scratch
60
+ * database, would be the one place a caller reaches for an extension operator
61
+ * class or a subquery, could not be checked against the collection's
62
+ * properties, and could not be fingerprinted — its own text would have to go
63
+ * into the derived name, so reformatting it would rename a live index.
64
+ *
65
+ * Structure keeps every reference resolvable at build time, keeps literals
66
+ * going through the same quoting as the rest of the DDL, and keeps the name
67
+ * stable under any rendering change.
68
+ *
69
+ * There is no `or`. An OR predicate almost always means the index should not
70
+ * be partial at all; a caller who genuinely needs one declares two indexes.
71
+ */
72
+ export type IndexPredicate<Keys extends string = string> = {
73
+ prop: Keys | (string & {});
74
+ op: "=";
75
+ value: string | number | boolean;
76
+ } | {
77
+ prop: Keys | (string & {});
78
+ op: "!=" | "<" | "<=" | ">" | ">=";
79
+ value: string | number;
80
+ } | {
81
+ prop: Keys | (string & {});
82
+ op: "is null" | "is not null";
83
+ }
84
+ /**
85
+ * A non-empty list, enforced in the type. An empty `IN` is a predicate
86
+ * matching nothing: it builds an index over zero rows and reports success,
87
+ * which is the silent-empty-condition shape this codebase has been bitten
88
+ * by before.
89
+ */
90
+ | {
91
+ prop: Keys | (string & {});
92
+ op: "in";
93
+ value: readonly [string | number, ...(string | number)[]];
94
+ } | {
95
+ and: readonly [IndexPredicate<Keys>, ...IndexPredicate<Keys>[]];
96
+ };
97
+ interface BaseCollectionIndex<Keys extends string = string> {
98
+ /**
99
+ * The key columns, in order. This *is* the index's identity.
100
+ *
101
+ * Postgres can only use a leading subset, so `["ownerId", "createdAt"]`
102
+ * serves a query filtering on `ownerId`, and one filtering on both, and
103
+ * never one filtering on `createdAt` alone.
104
+ *
105
+ * Capped at five keys. Postgres allows thirty-two; past four the trailing
106
+ * columns are dead weight on every write, and the declaration is usually
107
+ * someone hoping a query gets faster by accretion. Payload columns that
108
+ * are not searched belong in `include`, which does not count against this.
109
+ */
110
+ on: readonly [Keys | IndexKey<Keys>, ...(Keys | IndexKey<Keys>)[]];
111
+ where?: IndexPredicate<Keys>;
112
+ /**
113
+ * Why this index exists, in one line. Required, and the only required
114
+ * field carrying no SQL.
115
+ *
116
+ * An index is the only thing a Rebase config can declare that costs money
117
+ * forever and whose benefit is invisible from the config. `rebase doctor`
118
+ * prints this beside "0 scans in 34 days, 412 MB", which is the one moment
119
+ * anyone is in a position to decide whether to delete it. Without it
120
+ * nobody can decide, so nobody does, and the table accretes indexes for
121
+ * the life of the product.
122
+ */
123
+ reason: string;
124
+ }
125
+ /**
126
+ * The default. Answers equality, range, `ORDER BY`, and uniqueness.
127
+ */
128
+ export interface BtreeIndex<Keys extends string = string> extends BaseCollectionIndex<Keys> {
129
+ using?: "btree";
130
+ /**
131
+ * A composite uniqueness guarantee.
132
+ *
133
+ * Single-column uniqueness is `validation.unique` on the property, and
134
+ * declaring it here is refused rather than accepted as a synonym.
135
+ * `validation.unique` compiles to an inline `UNIQUE` whose backing index
136
+ * Postgres — not Rebase — names `<table>_<column>_key`. That name is in
137
+ * every deployed database, appears in no contract file, and no release can
138
+ * reach in and rename it.
139
+ */
140
+ unique?: boolean;
141
+ /**
142
+ * Payload columns carried in the leaf pages, for index-only scans. Not
143
+ * searchable and not ordered — they save a heap fetch at the cost of a
144
+ * fatter index. May not overlap `on`.
145
+ */
146
+ include?: readonly (Keys | (string & {}))[];
147
+ }
148
+ /**
149
+ * Containment over an `array` property or a JSONB `map`, using core operator
150
+ * classes only. Trigram and full-text search are `search:`, not this.
151
+ */
152
+ export interface GinIndex<Keys extends string = string> extends BaseCollectionIndex<Keys> {
153
+ using: "gin";
154
+ on: readonly [Keys | UnorderedIndexKey<Keys>, ...(Keys | UnorderedIndexKey<Keys>)[]];
155
+ }
156
+ /**
157
+ * A naturally-ordered column on an append-only table — tiny, and useless the
158
+ * moment rows arrive out of order.
159
+ */
160
+ export interface BrinIndex<Keys extends string = string> extends BaseCollectionIndex<Keys> {
161
+ using: "brin";
162
+ on: readonly [Keys | UnorderedIndexKey<Keys>, ...(Keys | UnorderedIndexKey<Keys>)[]];
163
+ }
164
+ /**
165
+ * An index on a collection's table.
166
+ *
167
+ * No `gist` and no `hash`: every interesting gist operator class ships in an
168
+ * extension, and hash indexes cannot be unique, composite, or ordered.
169
+ *
170
+ * The restriction to core Postgres is not conservatism, it is what keeps the
171
+ * whole model on the Atlas path. `rebase db push` materialises the desired
172
+ * state in a bare scratch database to plan against, `--exclude` does not
173
+ * suppress that replay, and `CREATE EXTENSION` cannot be put in the file — so
174
+ * an index needing `gin_trgm_ops` or `vector_cosine_ops` is refused at build
175
+ * time rather than emitted to fail later against a database the author has
176
+ * never heard of. Trigram search is `search:`; ANN is a `vector` property.
177
+ */
178
+ export type CollectionIndex<Keys extends string = string> = BtreeIndex<Keys> | GinIndex<Keys> | BrinIndex<Keys>;
179
+ export {};
@@ -9,7 +9,7 @@
9
9
  * They were spread across three places that had nothing to do with each other:
10
10
  * the `Table*` shapes sat in `websockets.ts`, next to the WebSocket frame types
11
11
  * they share no relationship with, and `PostgresPolicy` was declared twice — in
12
- * `@rebasepro/admin`'s RLS tab and again in `@rebasepro/studio`'s RLS editor,
12
+ * `@rebasepro/cms`'s RLS tab and again in `@rebasepro/studio`'s RLS editor,
13
13
  * the second with a comment explaining it was inline "to avoid depending on
14
14
  * @rebasepro/studio". Neither had to: this package is already a dependency of
15
15
  * both.
@@ -625,7 +625,7 @@ export interface MapProperty extends BaseProperty {
625
625
  * rest of the map's presentation options: `sortProperties` in
626
626
  * `@rebasepro/common` reads it recursively, and `@rebasepro/firebase` calls
627
627
  * that when it builds collections. A core package cannot read the admin
628
- * block — the field exists only once `@rebasepro/admin-types` is installed.
628
+ * block — the field exists only once `@rebasepro/cms-types` is installed.
629
629
  */
630
630
  propertiesOrder?: string[];
631
631
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/types",
3
3
  "type": "module",
4
- "version": "0.16.1-canary.gef08a6e",
4
+ "version": "0.17.0",
5
5
  "description": "Rebase type definitions — shared interfaces and controller types",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * There is no *type* for the block in this package any more, and that is the point:
5
5
  * `admin` is not declared on `BaseCollectionConfig` or on any property here, so a
6
- * BaaS install cannot even write one. `@rebasepro/admin-types` adds the field back by
6
+ * BaaS install cannot even write one. `@rebasepro/cms-types` adds the field back by
7
7
  * declaration merging, which is why installing it is what makes the admin surface
8
8
  * appear.
9
9
  *
@@ -15,7 +15,7 @@
15
15
  * Every key that belongs inside a collection's `admin` block, as data.
16
16
  *
17
17
  * The type that describes these fields is `AdminCollectionOptions` in
18
- * `@rebasepro/admin-types`, and it is erased at build time — but three runtime
18
+ * `@rebasepro/cms-types`, and it is erased at build time — but three runtime
19
19
  * consumers need the list, and two of them are core:
20
20
  *
21
21
  * - `serializeCollections`, to drop the block from the contract
@@ -25,7 +25,7 @@
25
25
  * backend ignores it and the panel never finds it again.
26
26
  * - the `collections-admin-block` codemod
27
27
  *
28
- * `@rebasepro/admin-types` re-exports this and asserts it names only real option
28
+ * `@rebasepro/cms-types` re-exports this and asserts it names only real option
29
29
  * keys; the count is pinned by a test there.
30
30
  *
31
31
  * @group Models
@@ -81,15 +81,15 @@ export type AdminCollectionKey = typeof ADMIN_COLLECTION_KEYS[number];
81
81
  * Every key that belongs inside a *property's* `admin` block, as data.
82
82
  *
83
83
  * The union of `AdminPropertyOptions` and its per-type extensions
84
- * (`AdminStringOptions`, `AdminArrayOptions`, …) in `@rebasepro/admin-types`.
84
+ * (`AdminStringOptions`, `AdminArrayOptions`, …) in `@rebasepro/cms-types`.
85
85
  * It lives here for the same reason {@link ADMIN_COLLECTION_KEYS} does: the
86
86
  * runtime consumers are core packages that the BaaS guard forbids from
87
- * importing `@rebasepro/admin-types`. Here it is the boot-time collection
87
+ * importing `@rebasepro/cms-types`. Here it is the boot-time collection
88
88
  * validator in `@rebasepro/server`, which has to tell "you left `readOnly` at
89
89
  * the top of the property, where nothing reads it" apart from "you invented a
90
90
  * key we have never heard of".
91
91
  *
92
- * `@rebasepro/admin-types` re-exports this and asserts it names only real
92
+ * `@rebasepro/cms-types` re-exports this and asserts it names only real
93
93
  * option keys.
94
94
  *
95
95
  * @group Models
@@ -138,7 +138,7 @@ export type AdminPropertyKey = typeof ADMIN_PROPERTY_KEYS[number];
138
138
  * in favour of the value the user changed away from.
139
139
  *
140
140
  * This lives here, next to the key lists, because it had two implementations —
141
- * `toAdminCollectionConfig` in `@rebasepro/admin-types` and `nestAdminKeys` in
141
+ * `toAdminCollectionConfig` in `@rebasepro/cms-types` and `nestAdminKeys` in
142
142
  * `@rebasepro/server`'s schema editor — that agreed on everything except that
143
143
  * precedence, which is the only part that decides whether a save is visible.
144
144
  *
@@ -9,6 +9,7 @@ import type { SecurityRule } from "./security_rules";
9
9
  import { getDataSourceCapabilities } from "./data_source";
10
10
  import type { WhereFilterOp, FilterValues, FilterPreset } from "./filter-operators";
11
11
  import type { SearchConfig } from "./search";
12
+ import type { CollectionIndex } from "./indexes";
12
13
 
13
14
  /**
14
15
  * Base interface containing all driver-agnostic collection properties.
@@ -323,6 +324,24 @@ export interface PostgresCollectionConfig<M extends Record<string, unknown> = Re
323
324
  * @see SearchConfig
324
325
  */
325
326
  search?: SearchConfig;
327
+
328
+ /**
329
+ * Ordinary indexes on this collection's table.
330
+ *
331
+ * Collection-level, not per-property, because an index over two columns
332
+ * has no single property to hang on and a partial index has none at all —
333
+ * and because a second declaration site for the single-column case would
334
+ * put the same object in two places. An index's identity is a column list
335
+ * in an order; the single-column case is a degenerate one, not a special
336
+ * one.
337
+ *
338
+ * `VectorProperty.index` stays where it is: an ANN structure is a property
339
+ * of the column's type, not of a query.
340
+ *
341
+ * Postgres-only, like {@link SearchConfig}: refused on another engine
342
+ * rather than silently ignored.
343
+ */
344
+ indexes?: readonly CollectionIndex<Extract<keyof M, string>>[];
326
345
  }
327
346
 
328
347
  /**
@@ -681,7 +700,7 @@ export interface AuthCollectionConfig {
681
700
  *
682
701
  * Set to `false` to disable, or pass a custom `EntityAction` to replace the UI.
683
702
  *
684
- * The object form is an `EntityAction` from `@rebasepro/admin-types`, typed
703
+ * The object form is an `EntityAction` from `@rebasepro/cms-types`, typed
685
704
  * here as `object` because it is a React component with admin controllers in
686
705
  * its props and nothing on the server reads it — only whether the built-in
687
706
  * action is injected, which is the boolean.
@@ -18,7 +18,7 @@
18
18
  *
19
19
  * The cost is that the return type is `unknown` rather than `ReactNode`, so a
20
20
  * function that returns something React could not render is accepted here.
21
- * `@rebasepro/admin-types` re-exports a `ReactComponentRef<P>` narrowed against
21
+ * `@rebasepro/cms-types` re-exports a `ReactComponentRef<P>` narrowed against
22
22
  * the real `React.ComponentType` for authoring and for the admin's internals,
23
23
  * which restores that check where it can be enforced.
24
24
  */
@@ -6,6 +6,7 @@ export * from "./properties";
6
6
  export * from "./admin_block";
7
7
  export * from "./collections";
8
8
  export * from "./search";
9
+ export * from "./indexes";
9
10
  export * from "./relations";
10
11
  export * from "./policy";
11
12
  export * from "./rls-functions";
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Ordinary indexes, declared on a collection.
3
+ *
4
+ * Distinct from the two index-shaped things Rebase already builds. A `search`
5
+ * block builds a GIN index over a generated `tsvector`, and a `vector`
6
+ * property builds an ANN index over an embedding; both are structures the
7
+ * *feature* owns and neither is a query the developer wrote. This is the plain
8
+ * case — the btree behind a `where` clause — which had no declaration site at
9
+ * all, so the only way to have one was to write it by hand, where the next
10
+ * `rebase db push` planned it away.
11
+ *
12
+ * Every form here is core Postgres, deliberately. See {@link CollectionIndex}.
13
+ */
14
+
15
+ /**
16
+ * A key column of an index whose access method has no ordering.
17
+ *
18
+ * `gin` and `brin` reject `ASC`/`DESC`/`NULLS` outright — Postgres answers
19
+ * `access method "gin" does not support ASC/DESC options` — so those methods
20
+ * take this narrower shape and the combination is unrepresentable rather than
21
+ * refused at build time.
22
+ */
23
+ export interface UnorderedIndexKey<Keys extends string = string> {
24
+ /**
25
+ * A property key on this collection — never a column name.
26
+ *
27
+ * Which column that resolves to depends on the property, and the two
28
+ * differ in exactly the case an index is most often wanted for: a
29
+ * `belongsTo` relation compiles to its resolved `localKey`
30
+ * (`primaryCategory` → `primary_category_id`), not to the snake-cased
31
+ * property key. Anything else resolves through `columnName`, or the
32
+ * snake-case default when it declares none.
33
+ *
34
+ * Writing the column name here would work for most properties and quietly
35
+ * index nothing for a foreign key, which is the one people reach for.
36
+ */
37
+ prop: Keys | (string & {});
38
+ }
39
+
40
+ /**
41
+ * A key column of an index, when its order matters.
42
+ *
43
+ * `direction` and `nulls` earn their place only when a query's `ORDER BY`
44
+ * mixes directions. A lone `DESC` index is redundant with its `ASC` twin —
45
+ * Postgres scans a btree backwards just as fast — and declaring both is
46
+ * refused.
47
+ *
48
+ * Writing the Postgres default down explicitly is free: the derived name
49
+ * hashes the *effective* order, so adding `direction: "asc"` to a column that
50
+ * was already ascending is not a redefinition and rebuilds nothing.
51
+ */
52
+ export interface IndexKey<Keys extends string = string> extends UnorderedIndexKey<Keys> {
53
+ direction?: "asc" | "desc";
54
+ /** Postgres's own default: `last` under `asc`, `first` under `desc`. */
55
+ nulls?: "first" | "last";
56
+ }
57
+
58
+ /**
59
+ * The rows a partial index covers.
60
+ *
61
+ * Structure rather than a SQL string, and this is the most load-bearing choice
62
+ * in the type. A string would be replayed verbatim by Atlas in a scratch
63
+ * database, would be the one place a caller reaches for an extension operator
64
+ * class or a subquery, could not be checked against the collection's
65
+ * properties, and could not be fingerprinted — its own text would have to go
66
+ * into the derived name, so reformatting it would rename a live index.
67
+ *
68
+ * Structure keeps every reference resolvable at build time, keeps literals
69
+ * going through the same quoting as the rest of the DDL, and keeps the name
70
+ * stable under any rendering change.
71
+ *
72
+ * There is no `or`. An OR predicate almost always means the index should not
73
+ * be partial at all; a caller who genuinely needs one declares two indexes.
74
+ */
75
+ export type IndexPredicate<Keys extends string = string> =
76
+ | { prop: Keys | (string & {}); op: "="; value: string | number | boolean }
77
+ | { prop: Keys | (string & {}); op: "!=" | "<" | "<=" | ">" | ">="; value: string | number }
78
+ | { prop: Keys | (string & {}); op: "is null" | "is not null" }
79
+ /**
80
+ * A non-empty list, enforced in the type. An empty `IN` is a predicate
81
+ * matching nothing: it builds an index over zero rows and reports success,
82
+ * which is the silent-empty-condition shape this codebase has been bitten
83
+ * by before.
84
+ */
85
+ | { prop: Keys | (string & {}); op: "in"; value: readonly [string | number, ...(string | number)[]] }
86
+ | { and: readonly [IndexPredicate<Keys>, ...IndexPredicate<Keys>[]] };
87
+
88
+ interface BaseCollectionIndex<Keys extends string = string> {
89
+ /**
90
+ * The key columns, in order. This *is* the index's identity.
91
+ *
92
+ * Postgres can only use a leading subset, so `["ownerId", "createdAt"]`
93
+ * serves a query filtering on `ownerId`, and one filtering on both, and
94
+ * never one filtering on `createdAt` alone.
95
+ *
96
+ * Capped at five keys. Postgres allows thirty-two; past four the trailing
97
+ * columns are dead weight on every write, and the declaration is usually
98
+ * someone hoping a query gets faster by accretion. Payload columns that
99
+ * are not searched belong in `include`, which does not count against this.
100
+ */
101
+ on: readonly [Keys | IndexKey<Keys>, ...(Keys | IndexKey<Keys>)[]];
102
+
103
+ where?: IndexPredicate<Keys>;
104
+
105
+ /**
106
+ * Why this index exists, in one line. Required, and the only required
107
+ * field carrying no SQL.
108
+ *
109
+ * An index is the only thing a Rebase config can declare that costs money
110
+ * forever and whose benefit is invisible from the config. `rebase doctor`
111
+ * prints this beside "0 scans in 34 days, 412 MB", which is the one moment
112
+ * anyone is in a position to decide whether to delete it. Without it
113
+ * nobody can decide, so nobody does, and the table accretes indexes for
114
+ * the life of the product.
115
+ */
116
+ reason: string;
117
+ }
118
+
119
+ /**
120
+ * The default. Answers equality, range, `ORDER BY`, and uniqueness.
121
+ */
122
+ export interface BtreeIndex<Keys extends string = string> extends BaseCollectionIndex<Keys> {
123
+ using?: "btree";
124
+
125
+ /**
126
+ * A composite uniqueness guarantee.
127
+ *
128
+ * Single-column uniqueness is `validation.unique` on the property, and
129
+ * declaring it here is refused rather than accepted as a synonym.
130
+ * `validation.unique` compiles to an inline `UNIQUE` whose backing index
131
+ * Postgres — not Rebase — names `<table>_<column>_key`. That name is in
132
+ * every deployed database, appears in no contract file, and no release can
133
+ * reach in and rename it.
134
+ */
135
+ unique?: boolean;
136
+
137
+ /**
138
+ * Payload columns carried in the leaf pages, for index-only scans. Not
139
+ * searchable and not ordered — they save a heap fetch at the cost of a
140
+ * fatter index. May not overlap `on`.
141
+ */
142
+ include?: readonly (Keys | (string & {}))[];
143
+ }
144
+
145
+ /**
146
+ * Containment over an `array` property or a JSONB `map`, using core operator
147
+ * classes only. Trigram and full-text search are `search:`, not this.
148
+ */
149
+ export interface GinIndex<Keys extends string = string> extends BaseCollectionIndex<Keys> {
150
+ using: "gin";
151
+ on: readonly [Keys | UnorderedIndexKey<Keys>, ...(Keys | UnorderedIndexKey<Keys>)[]];
152
+ }
153
+
154
+ /**
155
+ * A naturally-ordered column on an append-only table — tiny, and useless the
156
+ * moment rows arrive out of order.
157
+ */
158
+ export interface BrinIndex<Keys extends string = string> extends BaseCollectionIndex<Keys> {
159
+ using: "brin";
160
+ on: readonly [Keys | UnorderedIndexKey<Keys>, ...(Keys | UnorderedIndexKey<Keys>)[]];
161
+ }
162
+
163
+ /**
164
+ * An index on a collection's table.
165
+ *
166
+ * No `gist` and no `hash`: every interesting gist operator class ships in an
167
+ * extension, and hash indexes cannot be unique, composite, or ordered.
168
+ *
169
+ * The restriction to core Postgres is not conservatism, it is what keeps the
170
+ * whole model on the Atlas path. `rebase db push` materialises the desired
171
+ * state in a bare scratch database to plan against, `--exclude` does not
172
+ * suppress that replay, and `CREATE EXTENSION` cannot be put in the file — so
173
+ * an index needing `gin_trgm_ops` or `vector_cosine_ops` is refused at build
174
+ * time rather than emitted to fail later against a database the author has
175
+ * never heard of. Trigram search is `search:`; ANN is a `vector` property.
176
+ */
177
+ export type CollectionIndex<Keys extends string = string> =
178
+ | BtreeIndex<Keys>
179
+ | GinIndex<Keys>
180
+ | BrinIndex<Keys>;
@@ -9,7 +9,7 @@
9
9
  * They were spread across three places that had nothing to do with each other:
10
10
  * the `Table*` shapes sat in `websockets.ts`, next to the WebSocket frame types
11
11
  * they share no relationship with, and `PostgresPolicy` was declared twice — in
12
- * `@rebasepro/admin`'s RLS tab and again in `@rebasepro/studio`'s RLS editor,
12
+ * `@rebasepro/cms`'s RLS tab and again in `@rebasepro/studio`'s RLS editor,
13
13
  * the second with a comment explaining it was inline "to avoid depending on
14
14
  * @rebasepro/studio". Neither had to: this package is already a dependency of
15
15
  * both.
@@ -712,7 +712,7 @@ export interface MapProperty extends BaseProperty {
712
712
  * rest of the map's presentation options: `sortProperties` in
713
713
  * `@rebasepro/common` reads it recursively, and `@rebasepro/firebase` calls
714
714
  * that when it builds collections. A core package cannot read the admin
715
- * block — the field exists only once `@rebasepro/admin-types` is installed.
715
+ * block — the field exists only once `@rebasepro/cms-types` is installed.
716
716
  */
717
717
  propertiesOrder?: string[];
718
718
  /**