@rebasepro/admin 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.
Files changed (124) hide show
  1. package/README.md +1 -1
  2. package/dist/{CollectionEditorDialog-o7F_zbGY.js → CollectionEditorDialog-Dpc75wIZ.js} +56 -260
  3. package/dist/CollectionEditorDialog-Dpc75wIZ.js.map +1 -0
  4. package/dist/{PropertyEditView-DgB2b22q.js → PropertyEditView-DH3XZC2x.js} +2 -2
  5. package/dist/{PropertyEditView-DgB2b22q.js.map → PropertyEditView-DH3XZC2x.js.map} +1 -1
  6. package/dist/{RouterCollectionsStudioView-DaLRgPkj.js → RouterCollectionsStudioView-BZLq4MGp.js} +4 -4
  7. package/dist/{RouterCollectionsStudioView-DaLRgPkj.js.map → RouterCollectionsStudioView-BZLq4MGp.js.map} +1 -1
  8. package/dist/collection_editor/CollectionEditorDialogsContext.d.ts +1 -1
  9. package/dist/collection_editor/_cms_internals.d.ts +1 -1
  10. package/dist/collection_editor/extensibility_types.d.ts +1 -1
  11. package/dist/collection_editor/serializable_types.d.ts +40 -12
  12. package/dist/collection_editor/serializable_utils.d.ts +1 -1
  13. package/dist/collection_editor/ui/collection_editor/CollectionRLSTab.d.ts +1 -11
  14. package/dist/collection_editor_ui.js +4 -4
  15. package/dist/components/CollectionEditorDialogs.d.ts +2 -2
  16. package/dist/components/CollectionTableBinding/column_utils.d.ts +7 -1
  17. package/dist/components/CollectionViewBinding/CollectionListViewBinding.d.ts +2 -2
  18. package/dist/components/DefaultDrawer.d.ts +3 -3
  19. package/dist/components/DrawerNavigationGroup.d.ts +8 -1
  20. package/dist/components/DrawerNavigationItem.d.ts +7 -1
  21. package/dist/components/EditViewBinding.d.ts +1 -1
  22. package/dist/components/RebaseAdmin.d.ts +1 -1
  23. package/dist/components/RebaseAuthGate.d.ts +1 -1
  24. package/dist/components/RebaseLayout.d.ts +5 -1
  25. package/dist/components/RebaseNavigation.d.ts +2 -2
  26. package/dist/components/RebaseRouteDefs.d.ts +1 -1
  27. package/dist/components/RebaseShell.d.ts +6 -1
  28. package/dist/components/RelationSelector.d.ts +14 -0
  29. package/dist/components/SelectableTable/filters/RelationFilterField.d.ts +27 -0
  30. package/dist/components/SelectableTable/filters/null_filter.d.ts +46 -0
  31. package/dist/components/SidePanelProvider.d.ts +2 -2
  32. package/dist/components/app/Scaffold.d.ts +11 -0
  33. package/dist/components/field_configs.d.ts +1 -1
  34. package/dist/{export-BvqhjdEb.js → export-BzIpUdSK.js} +3 -3
  35. package/dist/export-BzIpUdSK.js.map +1 -0
  36. package/dist/form/EntityForm.d.ts +2 -2
  37. package/dist/form/EntityFormBinding.d.ts +5 -5
  38. package/dist/{history-Dz20hH38.js → history-g9688IaK.js} +2 -2
  39. package/dist/{history-Dz20hH38.js.map → history-g9688IaK.js.map} +1 -1
  40. package/dist/hooks/index.d.ts +1 -1
  41. package/dist/hooks/useAdminContext.d.ts +29 -0
  42. package/dist/hooks/useHistory.d.ts +7 -11
  43. package/dist/{import-DINSB-Vu.js → import-BjUG-cIY.js} +2 -2
  44. package/dist/{import-DINSB-Vu.js.map → import-BjUG-cIY.js.map} +1 -1
  45. package/dist/index.js +61 -38
  46. package/dist/index.js.map +1 -1
  47. package/dist/types/components/EntityFormProps.d.ts +1 -1
  48. package/dist/types/fields.d.ts +1 -1
  49. package/dist/{util-BhhfHY1M.js → util-B5fJm1FA.js} +296 -169
  50. package/dist/util-B5fJm1FA.js.map +1 -0
  51. package/package.json +9 -9
  52. package/src/collection_editor/CollectionEditorDialogsContext.tsx +1 -1
  53. package/src/collection_editor/ConfigControllerProvider.tsx +1 -1
  54. package/src/collection_editor/_cms_internals.ts +1 -1
  55. package/src/collection_editor/extensibility_types.ts +1 -1
  56. package/src/collection_editor/index.ts +2 -2
  57. package/src/collection_editor/serializable_types.ts +44 -13
  58. package/src/collection_editor/serializable_utils.ts +68 -59
  59. package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +1 -1
  60. package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +66 -41
  61. package/src/collection_editor/ui/collection_editor/EntityActionsEditTab.tsx +1 -1
  62. package/src/collection_editor/ui/collection_editor/GetCodeDialog.tsx +1 -1
  63. package/src/collection_editor/ui/collection_editor/SubcollectionsEditTab.tsx +2 -2
  64. package/src/components/CollectionEditorDialogs.tsx +2 -2
  65. package/src/components/CollectionTableBinding/CollectionRowActions.tsx +5 -4
  66. package/src/components/CollectionTableBinding/CollectionTableBinding.tsx +8 -2
  67. package/src/components/CollectionTableBinding/column_utils.tsx +19 -5
  68. package/src/components/CollectionTableBinding/table_bindings.tsx +10 -10
  69. package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +8 -7
  70. package/src/components/CollectionViewBinding/CollectionViewActions.tsx +2 -2
  71. package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +2 -2
  72. package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +2 -2
  73. package/src/components/DefaultDrawer.tsx +19 -8
  74. package/src/components/DeleteEntityDialog.tsx +2 -2
  75. package/src/components/DetailViewBinding.tsx +21 -1
  76. package/src/components/DrawerNavigationGroup.tsx +47 -4
  77. package/src/components/DrawerNavigationItem.tsx +19 -5
  78. package/src/components/EditFormActions.tsx +4 -3
  79. package/src/components/EditViewBinding.tsx +1 -1
  80. package/src/components/HomePage/ContentHomePage.tsx +4 -4
  81. package/src/components/RebaseAdmin.tsx +3 -3
  82. package/src/components/RebaseAuthGate.tsx +1 -1
  83. package/src/components/RebaseLayout.tsx +8 -2
  84. package/src/components/RebaseNavigation.tsx +9 -9
  85. package/src/components/RebaseRouteDefs.tsx +2 -2
  86. package/src/components/RebaseShell.tsx +10 -1
  87. package/src/components/RelationSelector.tsx +18 -3
  88. package/src/components/SelectableTable/filters/FilterFieldBinding.tsx +1 -1
  89. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +36 -9
  90. package/src/components/SelectableTable/filters/RelationFilterField.tsx +93 -17
  91. package/src/components/SelectableTable/filters/null_filter.ts +63 -0
  92. package/src/components/SidePanelProvider.tsx +2 -2
  93. package/src/components/app/Scaffold.tsx +16 -1
  94. package/src/data_export/export/ExportCollectionAction.tsx +2 -2
  95. package/src/form/EntityForm.tsx +2 -2
  96. package/src/form/EntityFormActions.tsx +2 -1
  97. package/src/form/EntityFormBinding.tsx +7 -7
  98. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +3 -3
  99. package/src/form/field_bindings/MultipleRelationFieldBinding.tsx +16 -5
  100. package/src/form/field_bindings/ReferenceFieldBinding.tsx +3 -3
  101. package/src/form/field_bindings/RelationFieldBinding.tsx +17 -6
  102. package/src/form/field_bindings/RepeatFieldBinding.tsx +2 -2
  103. package/src/hooks/index.ts +1 -1
  104. package/src/hooks/navigation/useNavigationResolution.ts +10 -4
  105. package/src/hooks/{useCMSContext.tsx → useAdminContext.tsx} +10 -10
  106. package/src/hooks/useHistory.ts +7 -11
  107. package/src/hooks/useResolvedNavigationFrom.tsx +2 -2
  108. package/src/index.ts +4 -4
  109. package/src/preview/PropertyPreview.tsx +6 -6
  110. package/src/preview/components/RelationPreview.tsx +1 -1
  111. package/src/preview/property_previews/ArrayOfMapsPreview.tsx +1 -1
  112. package/src/preview/property_previews/ArrayOfReferencesPreview.tsx +2 -2
  113. package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +2 -2
  114. package/src/preview/property_previews/SkeletonPropertyComponent.tsx +2 -2
  115. package/src/types/components/EntityFormProps.tsx +2 -2
  116. package/src/types/fields.tsx +1 -1
  117. package/dist/CollectionEditorDialog-o7F_zbGY.js.map +0 -1
  118. package/dist/collection_editor/pgColumnToProperty.d.ts +0 -7
  119. package/dist/editor/extensions/Image/index.d.ts +0 -6
  120. package/dist/export-BvqhjdEb.js.map +0 -1
  121. package/dist/hooks/useCMSContext.d.ts +0 -29
  122. package/dist/util-BhhfHY1M.js.map +0 -1
  123. package/src/collection_editor/pgColumnToProperty.ts +0 -291
  124. package/src/editor/extensions/Image/index.ts +0 -133
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/admin",
3
3
  "type": "module",
4
- "version": "0.11.1-canary.gfd39654",
4
+ "version": "0.12.0",
5
5
  "description": "Rebase CMS — content management views, forms, and routing",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -83,14 +83,14 @@
83
83
  "react-dropzone": "^15.0.0",
84
84
  "react-use-measure": "^2.1.7",
85
85
  "zod": "^4.4.3",
86
- "@rebasepro/common": "0.11.1-canary.gfd39654",
87
- "@rebasepro/forms": "0.11.1-canary.gfd39654",
88
- "@rebasepro/inference": "0.11.1-canary.gfd39654",
89
- "@rebasepro/utils": "0.11.1-canary.gfd39654",
90
- "@rebasepro/ui": "0.11.1-canary.gfd39654",
91
- "@rebasepro/app": "0.11.1-canary.gfd39654",
92
- "@rebasepro/types": "0.11.1-canary.gfd39654",
93
- "@rebasepro/admin-types": "0.11.1-canary.gfd39654"
86
+ "@rebasepro/admin-types": "0.12.0",
87
+ "@rebasepro/app": "0.12.0",
88
+ "@rebasepro/common": "0.12.0",
89
+ "@rebasepro/forms": "0.12.0",
90
+ "@rebasepro/types": "0.12.0",
91
+ "@rebasepro/inference": "0.12.0",
92
+ "@rebasepro/ui": "0.12.0",
93
+ "@rebasepro/utils": "0.12.0"
94
94
  },
95
95
  "peerDependencies": {
96
96
  "react": ">=19.0.0",
@@ -5,7 +5,7 @@ import React, { createContext, useContext } from "react";
5
5
  * sets and CollectionEditorDialogs reads.
6
6
  *
7
7
  * This indirection exists because ConfigControllerProvider is a root-scope
8
- * plugin provider that wraps <RebaseShell>, therefore living ABOVE the CMS
8
+ * plugin provider that wraps <RebaseShell>, therefore living ABOVE the admin
9
9
  * context providers (CollectionRegistryContext, NavigationStateContext,
10
10
  * UrlContext). The actual dialog components need those contexts, so they
11
11
  * must render inside the shell tree.
@@ -245,7 +245,7 @@ parentEntityIds,
245
245
  }, [onAnalyticsEvent, pathSuggestions]);
246
246
 
247
247
  // Build the dialog props objects that will be consumed by
248
- // CollectionEditorDialogs (rendered inside RebaseShell where CMS
248
+ // CollectionEditorDialogs (rendered inside RebaseShell where admin
249
249
  // contexts are available).
250
250
  const collectionDialogProps = useMemo(() => {
251
251
  if (!currentDialog) return undefined;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Internal re-exports for the collection editor module.
3
3
  * This avoids circular imports by pointing directly at source files
4
- * rather than going through the CMS barrel (index.ts).
4
+ * rather than going through the admin barrel (index.ts).
5
5
  */
6
6
 
7
7
  // Hooks
@@ -160,7 +160,7 @@ export interface CollectionEditorExtensionProps {
160
160
  * reading from React contexts.
161
161
  *
162
162
  * Use this when embedding the collection editor outside of the Rebase
163
- * CMS, where the Rebase context providers are not available.
163
+ * admin, where the Rebase context providers are not available.
164
164
  *
165
165
  * Default: false (standard Rebase mode).
166
166
  */
@@ -1,9 +1,9 @@
1
1
  // Collection Editor — moved from @rebasepro/studio
2
- // This module provides the visual schema/collection editor for the CMS.
2
+ // This module provides the visual schema/collection editor for the admin.
3
3
  //
4
4
  // IMPORTANT: Heavy UI components (CollectionEditorDialog, CollectionStudioView,
5
5
  // CollectionsStudioView, PropertyForm, PropertyFormDialog) are NOT re-exported
6
- // here to keep the main CMS bundle lean. Import them from
6
+ // here to keep the main admin bundle lean. Import them from
7
7
  // "@rebasepro/admin/collection_editor_ui" instead — they live in a separate chunk
8
8
  // that is loaded on demand.
9
9
 
@@ -23,7 +23,8 @@ import type {
23
23
  OnAction,
24
24
  PropertyConditions,
25
25
  PropertyValidationSchema,
26
- SecurityRule
26
+ SecurityRule,
27
+ WhereFilterOp
27
28
  } from "@rebasepro/types";
28
29
  import type {
29
30
  CollectionSize,
@@ -47,7 +48,9 @@ export interface SerializableAdminBaseOptions {
47
48
  disabled?: boolean | SerializablePropertyDisabledConfig;
48
49
  widthPercentage?: number;
49
50
  customProps?: unknown;
50
- // Field and Preview are dropped (ComponentRef is not serializable)
51
+ /** Narrows the filter operators offered for this property. Plain data. */
52
+ filterOperators?: readonly WhereFilterOp[];
53
+ // Field, Preview and Filter are dropped (ComponentRef is not serializable)
51
54
  }
52
55
 
53
56
  /** JSON-serializable version of `PropertyDisabledConfig`. Already serializable. */
@@ -113,7 +116,12 @@ export interface SerializableAdminStringOptions extends SerializableAdminBaseOpt
113
116
  markdown?: boolean;
114
117
  previewAsTag?: boolean;
115
118
  clearable?: boolean;
116
- url?: boolean | "image" | "video" | "audio" | "file";
119
+ /**
120
+ * How a string holding a URL is rendered. Was mirrored here as `url`, the
121
+ * name of the *core* flag that says the string is a URL at all — so this
122
+ * option had no serializable form and the core flag had two.
123
+ */
124
+ urlPreview?: "image" | "video" | "audio" | "file";
117
125
  }
118
126
 
119
127
  /** JSON-serializable version of `AdminNumberOptions`. */
@@ -134,18 +142,26 @@ export interface SerializableAdminVectorOptions extends SerializableAdminBaseOpt
134
142
  /** JSON-serializable version of `AdminReferenceOptions`. */
135
143
  export interface SerializableAdminReferenceOptions extends SerializableAdminBaseOptions {
136
144
  previewProperties?: string[];
145
+ fixedFilter?: FilterValues<string>;
146
+ includeId?: boolean;
147
+ includeEntityLink?: boolean;
137
148
  }
138
149
 
139
150
  /** JSON-serializable version of `AdminRelationOptions`. */
140
151
  export interface SerializableAdminRelationOptions extends SerializableAdminBaseOptions {
141
152
  previewProperties?: string[];
142
153
  widget?: "select" | "dialog";
154
+ fixedFilter?: FilterValues<string>;
155
+ includeId?: boolean;
156
+ includeEntityLink?: boolean;
143
157
  }
144
158
 
145
159
  /** JSON-serializable version of `AdminArrayOptions`. */
146
160
  export interface SerializableAdminArrayOptions extends SerializableAdminBaseOptions {
147
161
  expanded?: boolean;
148
162
  minimalistView?: boolean;
163
+ sortable?: boolean;
164
+ canAddElements?: boolean;
149
165
  }
150
166
 
151
167
  /** JSON-serializable version of `AdminMapOptions`. */
@@ -153,6 +169,7 @@ export interface SerializableAdminMapOptions extends SerializableAdminBaseOption
153
169
  expanded?: boolean;
154
170
  minimalistView?: boolean;
155
171
  spreadChildren?: boolean;
172
+ previewProperties?: string[];
156
173
  }
157
174
 
158
175
  // ── Serializable base property ────────────────────────────────────────
@@ -168,6 +185,12 @@ export interface SerializableBaseProperty {
168
185
  description?: string;
169
186
  propertyConfig?: string;
170
187
  columnName?: string;
188
+ /**
189
+ * Server-side guarantee that the column never reaches an API response — a
190
+ * password hash, a verification token. Absent from this mirror until now,
191
+ * so editing a collection in the panel silently unset it.
192
+ */
193
+ excludeFromApi?: boolean;
171
194
  validation?: PropertyValidationSchema;
172
195
  defaultValue?: unknown;
173
196
  /**
@@ -196,6 +219,8 @@ export interface SerializableStringProperty extends SerializableBaseProperty {
196
219
  storage?: SerializableStorageConfig;
197
220
  userSelect?: boolean;
198
221
  email?: boolean;
222
+ /** The string holds a URL. Feeds the generated OpenAPI contract. */
223
+ url?: boolean;
199
224
  reference?: SerializableReferenceProperty;
200
225
  }
201
226
 
@@ -257,9 +282,6 @@ export interface SerializableReferenceProperty extends SerializableBaseProperty
257
282
  type: "reference";
258
283
  isId?: boolean;
259
284
  path?: string;
260
- fixedFilter?: FilterValues<string>;
261
- includeId?: boolean;
262
- includeEntityLink?: boolean;
263
285
  }
264
286
 
265
287
  /**
@@ -309,10 +331,6 @@ export interface SerializableRelationProperty extends SerializableBaseProperty {
309
331
  * went on describing a shape nothing produced, and the cast hid it.
310
332
  */
311
333
  relation?: SerializableRelation;
312
- fixedFilter?: FilterValues<string>;
313
- includeId?: boolean;
314
- includeEntityLink?: boolean;
315
- widget?: "select" | "dialog";
316
334
  // overrides are dropped (can contain non-serializable CollectionConfig fields)
317
335
  }
318
336
 
@@ -329,8 +347,6 @@ export interface SerializableArrayProperty extends SerializableBaseProperty {
329
347
  valueField?: string;
330
348
  };
331
349
  validation?: ArrayPropertyValidationSchema;
332
- sortable?: boolean;
333
- canAddElements?: boolean;
334
350
  }
335
351
 
336
352
  /** JSON-serializable `MapProperty`. */
@@ -341,7 +357,6 @@ export interface SerializableMapProperty extends SerializableBaseProperty {
341
357
  properties?: SerializableProperties;
342
358
  propertiesOrder?: string[];
343
359
  validation?: PropertyValidationSchema;
344
- previewProperties?: string[];
345
360
  keyValue?: boolean;
346
361
  }
347
362
 
@@ -453,7 +468,23 @@ export interface SerializableCollectionConfig {
453
468
  // ── SQL / Postgres-specific ───────────────────────────────────────
454
469
  table?: string;
455
470
  schema?: string;
471
+ /**
472
+ * Collection-level relations, `target` as a slug — a thunk does not
473
+ * serialize, same as on a relation property.
474
+ *
475
+ * Absent from this mirror until now, and `buildCollectionFromTableMetadata`
476
+ * is the reason it matters: importing a table detects its foreign keys and
477
+ * junctions, puts them on the form, and the save then dropped every one of
478
+ * them. The import looked like it worked.
479
+ */
480
+ relations?: SerializableRelation[];
456
481
  securityRules?: readonly SecurityRule[];
482
+ /** Removes the framework's injected baseline RLS policies. */
483
+ disableDefaultPolicies?: boolean;
484
+
485
+ // ── Write behaviour ───────────────────────────────────────────────
486
+ /** Whether a write naming an undeclared field is rejected with a 400. */
487
+ strictWrites?: boolean;
457
488
 
458
489
  // ── Owner ─────────────────────────────────────────────────────────
459
490
  ownerId?: string;
@@ -8,7 +8,7 @@
8
8
  * @module
9
9
  */
10
10
 
11
- import type { ArrayProperty, BinaryProperty, BooleanProperty, DateProperty, GeopointProperty, MapProperty, NumberProperty, Properties, Property, ReferenceProperty, RelationProperty, StorageConfig, StringProperty, VectorProperty } from "@rebasepro/types";
11
+ import { isRelationalCollectionConfig, type Relation, type ArrayProperty, type BinaryProperty, type BooleanProperty, type DateProperty, type GeopointProperty, type MapProperty, type NumberProperty, type Properties, type Property, type ReferenceProperty, type RelationProperty, type StorageConfig, type StringProperty, type VectorProperty } from "@rebasepro/types";
12
12
  import type { AdminPropertyOptions } from "@rebasepro/admin-types";
13
13
  import type { AdminCollection } from "@rebasepro/admin-types";
14
14
 
@@ -27,6 +27,7 @@ import type {
27
27
  SerializableProperties,
28
28
  SerializableProperty,
29
29
  SerializableReferenceProperty,
30
+ SerializableRelation,
30
31
  SerializableRelationProperty,
31
32
  SerializableStorageConfig,
32
33
  SerializableStringProperty,
@@ -51,13 +52,22 @@ export type CollectionLookup = (slug: string) => AdminCollection | undefined;
51
52
  * Strip non-serializable fields from a `AdminPropertyOptions`.
52
53
  * Removes `Field` and `Preview` (ComponentRef).
53
54
  */
55
+ /**
56
+ * Every `ComponentRef` on a property's admin block. These are components, not
57
+ * data, and none of them survives `JSON.stringify` — a function-valued key is
58
+ * dropped from the output object with no error, so a component written here
59
+ * would leave a key that reads back as missing rather than as itself.
60
+ *
61
+ * `Filter` was absent from this list while `Field` and `Preview` were named
62
+ * twice, once destructured and once compared by string.
63
+ */
64
+ const NON_SERIALIZABLE_ADMIN_KEYS = ["Field", "Preview", "Filter"];
65
+
54
66
  function toSerializableAdminOptions(ui: AdminPropertyOptions | undefined): SerializableAdminBaseOptions | undefined {
55
67
  if (!ui) return undefined;
56
- const { Field, Preview, ...rest } = ui as AdminPropertyOptions & Record<string, unknown>;
57
- // Only return if there are remaining fields
58
68
  const result: Record<string, unknown> = {};
59
- for (const [key, value] of Object.entries(rest)) {
60
- if (key !== "Field" && key !== "Preview" && value !== undefined) {
69
+ for (const [key, value] of Object.entries(ui as Record<string, unknown>)) {
70
+ if (!NON_SERIALIZABLE_ADMIN_KEYS.includes(key) && value !== undefined) {
61
71
  result[key] = value;
62
72
  }
63
73
  }
@@ -121,6 +131,48 @@ function resolveRelationTarget(target: RelationProperty["relation"] extends infe
121
131
  return undefined;
122
132
  }
123
133
 
134
+ /**
135
+ * A `Relation` as JSON: `target` resolved from its thunk down to a slug.
136
+ *
137
+ * Switched on `kind` and assigned without a cast, so the compiler checks that
138
+ * each branch writes the fields its kind owns and no others. This used to be
139
+ * built as an untyped bag and cast into place — which is how the target
140
+ * interface drifted a whole refactor behind the code writing to it.
141
+ *
142
+ * Shared by the relation *property* and the collection-level `relations` array,
143
+ * which had no serializer at all: `buildCollectionFromTableMetadata` fills that
144
+ * array from the table's foreign keys and junctions on import, and every one of
145
+ * them was dropped on save.
146
+ */
147
+ function toSerializableRelation(link: Relation): SerializableRelation {
148
+ const target = resolveRelationTarget(link.target);
149
+ const common = {
150
+ ...(target ? { target } : {}),
151
+ ...(link.relationName ? { relationName: link.relationName } : {}),
152
+ ...(link.onUpdate ? { onUpdate: link.onUpdate } : {}),
153
+ ...(link.onDelete ? { onDelete: link.onDelete } : {})
154
+ };
155
+ switch (link.kind) {
156
+ case "belongsTo":
157
+ return { kind: "belongsTo", ...common, ...(link.localKey ? { localKey: link.localKey } : {}) };
158
+ case "hasOne":
159
+ case "hasMany":
160
+ return {
161
+ kind: link.kind,
162
+ ...common,
163
+ ...(link.foreignKeyOnTarget ? { foreignKeyOnTarget: link.foreignKeyOnTarget } : {})
164
+ };
165
+ case "manyToMany":
166
+ return { kind: "manyToMany", ...common, ...(link.through ? { through: link.through } : {}) };
167
+ case "via":
168
+ return { kind: "via", ...common, cardinality: link.cardinality, joinPath: link.joinPath };
169
+ default: {
170
+ const exhaustive: never = link;
171
+ throw new Error(`Unhandled relation kind: ${JSON.stringify(exhaustive)}`);
172
+ }
173
+ }
174
+ }
175
+
124
176
  /**
125
177
  * Strip non-serializable base property fields.
126
178
  * Returns a clean object with only the common serializable fields from BaseProperty.
@@ -134,6 +186,7 @@ function toSerializableBaseFields(property: Property): Omit<SerializableBaseProp
134
186
  if (property.description) result.description = property.description;
135
187
  if (property.propertyConfig) result.propertyConfig = property.propertyConfig;
136
188
  if (property.columnName) result.columnName = property.columnName;
189
+ if (property.excludeFromApi !== undefined) result.excludeFromApi = property.excludeFromApi;
137
190
  if (property.defaultValue !== undefined) result.defaultValue = property.defaultValue;
138
191
  if (property.conditions) result.conditions = property.conditions;
139
192
  if (property.metadata) result.metadata = property.metadata;
@@ -164,6 +217,7 @@ export function toSerializableProperty(property: Property): SerializableProperty
164
217
  if (sp.enum) result.enum = sp.enum;
165
218
  if (sp.userSelect) result.userSelect = sp.userSelect;
166
219
  if (sp.email) result.email = sp.email;
220
+ if (sp.url) result.url = sp.url;
167
221
 
168
222
  // Convert validation.matches from RegExp to string
169
223
  if (sp.validation) {
@@ -255,9 +309,6 @@ export function toSerializableProperty(property: Property): SerializableProperty
255
309
  };
256
310
  if (rp.isId !== undefined) result.isId = rp.isId;
257
311
  if (rp.path) result.path = rp.path;
258
- if (rp.fixedFilter) result.fixedFilter = rp.fixedFilter;
259
- if (rp.includeId !== undefined) result.includeId = rp.includeId;
260
- if (rp.includeEntityLink !== undefined) result.includeEntityLink = rp.includeEntityLink;
261
312
  return result;
262
313
  }
263
314
 
@@ -278,54 +329,8 @@ export function toSerializableProperty(property: Property): SerializableProperty
278
329
  // no others. This used to be built as an untyped bag and cast
279
330
  // into place — which is how the target interface drifted a
280
331
  // whole refactor behind the code writing to it.
281
- const target = resolveRelationTarget(link.target);
282
- const common = {
283
- ...(target ? { target } : {}),
284
- ...(link.relationName ? { relationName: link.relationName } : {}),
285
- ...(link.onUpdate ? { onUpdate: link.onUpdate } : {}),
286
- ...(link.onDelete ? { onDelete: link.onDelete } : {})
287
- };
288
- switch (link.kind) {
289
- case "belongsTo":
290
- result.relation = {
291
- kind: "belongsTo",
292
- ...common,
293
- ...(link.localKey ? { localKey: link.localKey } : {})
294
- };
295
- break;
296
- case "hasOne":
297
- case "hasMany":
298
- result.relation = {
299
- kind: link.kind,
300
- ...common,
301
- ...(link.foreignKeyOnTarget ? { foreignKeyOnTarget: link.foreignKeyOnTarget } : {})
302
- };
303
- break;
304
- case "manyToMany":
305
- result.relation = {
306
- kind: "manyToMany",
307
- ...common,
308
- ...(link.through ? { through: link.through } : {})
309
- };
310
- break;
311
- case "via":
312
- result.relation = {
313
- kind: "via",
314
- ...common,
315
- cardinality: link.cardinality,
316
- joinPath: link.joinPath
317
- };
318
- break;
319
- default: {
320
- const exhaustive: never = link;
321
- throw new Error(`Unhandled relation kind: ${JSON.stringify(exhaustive)}`);
322
- }
323
- }
332
+ result.relation = toSerializableRelation(link);
324
333
  }
325
- if (rl.fixedFilter) result.fixedFilter = rl.fixedFilter;
326
- if (rl.includeId !== undefined) result.includeId = rl.includeId;
327
- if (rl.includeEntityLink !== undefined) result.includeEntityLink = rl.includeEntityLink;
328
- if (rl.widget) result.widget = rl.widget;
329
334
  // overrides are dropped (may hold a non-serializable CollectionConfig)
330
335
  return result;
331
336
  }
@@ -338,8 +343,6 @@ export function toSerializableProperty(property: Property): SerializableProperty
338
343
  };
339
344
  if (ap.columnType) result.columnType = ap.columnType;
340
345
  if (ap.validation) result.validation = ap.validation;
341
- if (ap.sortable !== undefined) result.sortable = ap.sortable;
342
- if (ap.canAddElements !== undefined) result.canAddElements = ap.canAddElements;
343
346
 
344
347
  // Recursively serialize the "of" property
345
348
  if (ap.of) {
@@ -371,7 +374,6 @@ export function toSerializableProperty(property: Property): SerializableProperty
371
374
  if (mp.columnType) result.columnType = mp.columnType;
372
375
  if (mp.validation) result.validation = mp.validation;
373
376
  if (mp.propertiesOrder) result.propertiesOrder = mp.propertiesOrder;
374
- if (mp.previewProperties) result.previewProperties = mp.previewProperties;
375
377
  if (mp.keyValue) result.keyValue = mp.keyValue;
376
378
 
377
379
  // Recursively serialize nested properties
@@ -448,7 +450,10 @@ export function toSerializableCollectionConfig(collection: AdminCollection): Ser
448
450
  if (collection.titleProperty) result.titleProperty = collection.titleProperty as string;
449
451
  if (collection.ownerId) result.ownerId = collection.ownerId;
450
452
  if (collection.metadata) result.metadata = collection.metadata;
451
- if (collection.table) result.table = collection.table;
453
+ if (isRelationalCollectionConfig(collection) && collection.table) result.table = collection.table;
454
+ if (isRelationalCollectionConfig(collection) && collection.relations?.length) {
455
+ result.relations = collection.relations.map(toSerializableRelation);
456
+ }
452
457
  if ("schema" in collection && collection.schema) result.schema = collection.schema as string;
453
458
  if (collection.orderProperty) result.orderProperty = collection.orderProperty as string;
454
459
 
@@ -462,6 +467,10 @@ export function toSerializableCollectionConfig(collection: AdminCollection): Ser
462
467
  if (collection.enabledViews) result.enabledViews = collection.enabledViews;
463
468
  if (collection.disableDefaultActions) result.disableDefaultActions = collection.disableDefaultActions;
464
469
  if (collection.securityRules) result.securityRules = collection.securityRules;
470
+ if (isRelationalCollectionConfig(collection) && collection.disableDefaultPolicies !== undefined) {
471
+ result.disableDefaultPolicies = collection.disableDefaultPolicies;
472
+ }
473
+ if (collection.strictWrites !== undefined) result.strictWrites = collection.strictWrites;
465
474
 
466
475
  // Enum-like fields
467
476
  if (collection.openEntityMode) result.openEntityMode = collection.openEntityMode;
@@ -53,7 +53,7 @@ import { AICollectionGeneratorPopover } from "./AICollectionGeneratorPopover";
53
53
  import { AIModifiedPathsProvider, useAIModifiedPaths } from "./AIModifiedPathsContext";
54
54
  import { CollectionGenerationCallback, CollectionOperation } from "../../api/generateCollectionApi";
55
55
  import { CollectionRLSTab } from "./CollectionRLSTab";
56
- import { buildCollectionFromTableMetadata } from "../../pgColumnToProperty";
56
+ import { buildCollectionFromTableMetadata } from "@rebasepro/common";
57
57
  import { mergeDeep, randomString, removeUndefined } from "@rebasepro/utils";
58
58
  import type { CollectionEditorExtensionProps, CollectionEditorTab } from "../../extensibility_types";
59
59
 
@@ -1,5 +1,4 @@
1
1
 
2
- import { SecurityRule as GeneratedSecurityRule } from "@rebasepro/types";
3
2
  import { getPolicyNamesForRule, getPolicyNamesForRules } from "@rebasepro/utils";
4
3
  import { getEffectiveSecurityRules } from "@rebasepro/common";
5
4
  import React, { useState, useEffect, useMemo } from "react";
@@ -42,27 +41,9 @@ import {
42
41
  import { useFormex } from "@rebasepro/forms";
43
42
  import { useRebaseContext } from "@rebasepro/app";
44
43
  import type { AdminCollection } from "@rebasepro/admin-types";
44
+ import type { PostgresPolicy, SecurityOperation, SecurityRule } from "@rebasepro/types";
45
45
 
46
- /** Postgres RLS policy shape defined inline to avoid depending on @rebasepro/studio */
47
- export interface PostgresPolicy {
48
- policyname: string;
49
- tablename: string;
50
- permissive: "PERMISSIVE" | "RESTRICTIVE";
51
- roles: string[];
52
- cmd: "SELECT" | "INSERT" | "UPDATE" | "DELETE" | "ALL";
53
- qual: string | null;
54
- with_check: string | null;
55
- status?: "live" | "code_only" | "both";
56
- }
57
-
58
- interface SecurityRule {
59
- name: string;
60
- operation?: string;
61
- mode?: string;
62
- using?: string;
63
- withCheck?: string;
64
- roles?: string[];
65
- }
46
+ export type { PostgresPolicy } from "@rebasepro/types";
66
47
 
67
48
  type CollectionWithSecurity = AdminCollection & {
68
49
  securityRules?: SecurityRule[];
@@ -71,6 +52,64 @@ type CollectionWithSecurity = AdminCollection & {
71
52
  alias?: string;
72
53
  };
73
54
 
55
+ /**
56
+ * `pg_policies` reports the operation and the permissive flag in upper case;
57
+ * `SecurityRule` names them in lower case. These two used to be `.toLowerCase()`
58
+ * assigned straight into a `SecurityRule`, which typechecks only against a
59
+ * `string` field — and this file declared its own local `SecurityRule` with
60
+ * `operation?: string`, so it did. Against the real type it does not: an
61
+ * unrecognised `cmd` would have been written into the collection as a policy
62
+ * operation that compiles to nothing.
63
+ */
64
+ function toSecurityOperation(cmd: PostgresPolicy["cmd"] | undefined): SecurityOperation | undefined {
65
+ switch (cmd) {
66
+ case "SELECT": return "select";
67
+ case "INSERT": return "insert";
68
+ case "UPDATE": return "update";
69
+ case "DELETE": return "delete";
70
+ case "ALL": return "all";
71
+ default: return undefined;
72
+ }
73
+ }
74
+
75
+ function toSecurityMode(permissive: PostgresPolicy["permissive"] | undefined): SecurityRule["mode"] {
76
+ // Absent stays absent. `permissive` is the default, so writing it out
77
+ // changes nothing semantically — but this value is serialized into the
78
+ // user's collection file, and defaulting here would grow a `mode:
79
+ // "permissive"` on every rule the editor touches. The `.toLowerCase()`
80
+ // this replaced returned `undefined` for an absent value; that part was
81
+ // right, and only the widened type was wrong.
82
+ if (!permissive) return undefined;
83
+ return permissive === "RESTRICTIVE" ? "restrictive" : "permissive";
84
+ }
85
+
86
+ /**
87
+ * Build a {@link SecurityRule} from a `pg_policies` row.
88
+ *
89
+ * `SecurityRule` is a discriminated union and this is the part the local shadow
90
+ * type flattened away: a raw-SQL rule *requires* `using`, so a policy with only
91
+ * a `WITH CHECK` clause — every INSERT-only policy — is not one. It is a
92
+ * roles-only rule carrying the check. Assembling the object field-by-field
93
+ * against `operation?: string` accepted all of these and told the compiler
94
+ * nothing.
95
+ */
96
+ function toSecurityRule(policy: Partial<PostgresPolicy>): SecurityRule {
97
+ const base = {
98
+ name: policy.policyname ?? "",
99
+ operation: toSecurityOperation(policy.cmd),
100
+ mode: toSecurityMode(policy.permissive),
101
+ roles: policy.roles ? [...policy.roles] : undefined
102
+ };
103
+ if (policy.qual) {
104
+ return {
105
+ ...base,
106
+ using: policy.qual,
107
+ withCheck: policy.with_check || undefined
108
+ };
109
+ }
110
+ return base;
111
+ }
112
+
74
113
  export function CollectionRLSTab() {
75
114
  const { values, setFieldValue } = useFormex<CollectionWithSecurity>();
76
115
  const [editingPolicy, setEditingPolicy] = useState<PostgresPolicy | "new" | null>(null);
@@ -186,7 +225,7 @@ export function CollectionRLSTab() {
186
225
  if (!tableName) return new Set<string>();
187
226
  const effectiveRules = getEffectiveSecurityRules(values as AdminCollection);
188
227
  return getPolicyNamesForRules(
189
- [...(rules as unknown as GeneratedSecurityRule[]), ...effectiveRules],
228
+ [...rules, ...effectiveRules],
190
229
  tableName
191
230
  );
192
231
  }, [rules, tableName, values]);
@@ -194,14 +233,7 @@ export function CollectionRLSTab() {
194
233
  const unmappedPolicies = dbPolicies.filter(dp => !generatedPolicyNames.has(dp.policyname));
195
234
 
196
235
  const handleSave = async (newPolicy: Partial<PostgresPolicy>) => {
197
- const rule: SecurityRule = {
198
- name: newPolicy.policyname ?? "",
199
- operation: newPolicy.cmd?.toLowerCase(),
200
- mode: newPolicy.permissive?.toLowerCase(),
201
- using: newPolicy.qual || undefined,
202
- withCheck: newPolicy.with_check || undefined,
203
- roles: newPolicy.roles
204
- };
236
+ const rule: SecurityRule = toSecurityRule(newPolicy);
205
237
 
206
238
  let newRules;
207
239
  if (editingPolicy === "new") {
@@ -240,7 +272,7 @@ export function CollectionRLSTab() {
240
272
  {/* Unnamed rules are still named in Postgres — show what they compile to
241
273
  rather than an empty heading. */}
242
274
  {rule.name || (tableName
243
- ? getPolicyNamesForRule(rule as unknown as GeneratedSecurityRule, tableName).join(", ")
275
+ ? getPolicyNamesForRule(rule, tableName).join(", ")
244
276
  : "Unnamed policy")}
245
277
  </Typography>
246
278
  </div>
@@ -251,11 +283,11 @@ export function CollectionRLSTab() {
251
283
  </div>
252
284
  <div className="flex items-center gap-1 sm:gap-2 shrink-0">
253
285
  <Button size="small" variant="text" onClick={() => setEditingPolicy({
254
- policyname: rule.name,
286
+ policyname: rule.name ?? "",
255
287
  tablename: values.id || values.table || values.alias || "your_table",
256
288
  permissive: (rule.mode || "permissive").toUpperCase() as PostgresPolicy["permissive"],
257
289
  cmd: (rule.operation || "ALL").toUpperCase() as PostgresPolicy["cmd"],
258
- roles: rule.roles || [],
290
+ roles: [...(rule.roles ?? [])],
259
291
  qual: rule.using || null,
260
292
  with_check: rule.withCheck || null
261
293
  })}>
@@ -305,14 +337,7 @@ export function CollectionRLSTab() {
305
337
  </div>
306
338
  <div className="flex items-center gap-1 sm:gap-2 shrink-0">
307
339
  <Button size="small" variant="outlined" color="primary" onClick={() => {
308
- const rule: SecurityRule = {
309
- name: dp.policyname,
310
- operation: dp.cmd?.toLowerCase(),
311
- mode: dp.permissive?.toLowerCase(),
312
- using: dp.qual || undefined,
313
- withCheck: dp.with_check || undefined,
314
- roles: dp.roles
315
- };
340
+ const rule: SecurityRule = toSecurityRule(dp);
316
341
  setFieldValue("securityRules", [...rules, rule]);
317
342
  }}>
318
343
  Import to codebase
@@ -145,7 +145,7 @@ export function EntityActionsEditTab({
145
145
  title={<>Remove this action?</>}
146
146
  body={<>This will <b>not
147
147
  delete any data</b>, only
148
- the action in the CMS</>}/>}
148
+ the action in the admin</>}/>}
149
149
 
150
150
  <EntityActionsSelectDialog
151
151
  open={addEntityActionDialogOpen}
@@ -27,7 +27,7 @@ export function GetCodeDialog({
27
27
  <DialogContent>
28
28
 
29
29
  <Typography variant={"body2"} className={"my-4 mb-8"}>
30
- If you want to customise the collection in code, you can add this collection code to your CMS
30
+ If you want to customise the collection in code, you can add this collection code to your admin
31
31
  app configuration.
32
32
  More info in the <a
33
33
  rel="noopener noreferrer"
@@ -214,7 +214,7 @@ export function SubcollectionsEditTab({
214
214
  title={<>Delete this subcollection?</>}
215
215
  body={<> This will <b>not
216
216
  delete any data</b>, only
217
- the collection in the CMS</>}/>}
217
+ the collection in the admin</>}/>}
218
218
  {viewToDelete &&
219
219
  <ConfirmationDialog open={Boolean(viewToDelete)}
220
220
  onAccept={() => {
@@ -225,7 +225,7 @@ export function SubcollectionsEditTab({
225
225
  title={<>Remove this view?</>}
226
226
  body={<>This will <b>not
227
227
  delete any data</b>, only
228
- the view in the CMS</>}/>}
228
+ the view in the admin</>}/>}
229
229
 
230
230
  <CollectionEditorDialog
231
231
  open={Boolean(currentDialog)}