@firecms/collection_editor 3.0.1 → 3.1.0-canary.768c91f

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 (90) hide show
  1. package/dist/ConfigControllerProvider.d.ts +6 -0
  2. package/dist/api/generateCollectionApi.d.ts +71 -0
  3. package/dist/api/index.d.ts +1 -0
  4. package/dist/index.d.ts +5 -1
  5. package/dist/index.es.js +9466 -5588
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/index.umd.js +9461 -5583
  8. package/dist/index.umd.js.map +1 -1
  9. package/dist/types/collection_editor_controller.d.ts +14 -0
  10. package/dist/types/collection_inference.d.ts +8 -2
  11. package/dist/types/config_controller.d.ts +23 -2
  12. package/dist/ui/AddKanbanColumnAction.d.ts +11 -0
  13. package/dist/ui/KanbanSetupAction.d.ts +10 -0
  14. package/dist/ui/collection_editor/AICollectionGeneratorPopover.d.ts +37 -0
  15. package/dist/ui/collection_editor/AIModifiedPathsContext.d.ts +20 -0
  16. package/dist/ui/collection_editor/CollectionDetailsForm.d.ts +2 -3
  17. package/dist/ui/collection_editor/CollectionEditorDialog.d.ts +24 -0
  18. package/dist/ui/collection_editor/CollectionEditorWelcomeView.d.ts +4 -1
  19. package/dist/ui/collection_editor/CollectionJsonImportDialog.d.ts +7 -0
  20. package/dist/ui/collection_editor/CollectionYupValidation.d.ts +9 -13
  21. package/dist/ui/collection_editor/DisplaySettingsForm.d.ts +3 -0
  22. package/dist/ui/collection_editor/EntityActionsEditTab.d.ts +2 -1
  23. package/dist/ui/collection_editor/ExtendSettingsForm.d.ts +14 -0
  24. package/dist/ui/collection_editor/GeneralSettingsForm.d.ts +7 -0
  25. package/dist/ui/collection_editor/KanbanConfigSection.d.ts +4 -0
  26. package/dist/ui/collection_editor/PropertyEditView.d.ts +6 -1
  27. package/dist/ui/collection_editor/PropertyTree.d.ts +2 -1
  28. package/dist/ui/collection_editor/SubcollectionsEditTab.d.ts +2 -1
  29. package/dist/ui/collection_editor/ViewModeSwitch.d.ts +6 -0
  30. package/dist/ui/collection_editor/properties/EnumPropertyField.d.ts +2 -1
  31. package/dist/ui/collection_editor/properties/conditions/ConditionsEditor.d.ts +10 -0
  32. package/dist/ui/collection_editor/properties/conditions/ConditionsPanel.d.ts +2 -0
  33. package/dist/ui/collection_editor/properties/conditions/EnumConditionsEditor.d.ts +6 -0
  34. package/dist/ui/collection_editor/properties/conditions/index.d.ts +6 -0
  35. package/dist/ui/collection_editor/properties/conditions/property_paths.d.ts +19 -0
  36. package/dist/useCollectionEditorPlugin.d.ts +7 -1
  37. package/dist/utils/validateCollectionJson.d.ts +22 -0
  38. package/package.json +15 -15
  39. package/src/ConfigControllerProvider.tsx +82 -47
  40. package/src/api/generateCollectionApi.ts +119 -0
  41. package/src/api/index.ts +1 -0
  42. package/src/index.ts +28 -1
  43. package/src/types/collection_editor_controller.tsx +16 -3
  44. package/src/types/collection_inference.ts +15 -2
  45. package/src/types/config_controller.tsx +27 -2
  46. package/src/ui/AddKanbanColumnAction.tsx +203 -0
  47. package/src/ui/EditorCollectionActionStart.tsx +1 -2
  48. package/src/ui/HomePageEditorCollectionAction.tsx +41 -13
  49. package/src/ui/KanbanSetupAction.tsx +38 -0
  50. package/src/ui/MissingReferenceWidget.tsx +1 -1
  51. package/src/ui/NewCollectionButton.tsx +1 -1
  52. package/src/ui/PropertyAddColumnComponent.tsx +1 -1
  53. package/src/ui/collection_editor/AICollectionGeneratorPopover.tsx +242 -0
  54. package/src/ui/collection_editor/AIModifiedPathsContext.tsx +88 -0
  55. package/src/ui/collection_editor/CollectionDetailsForm.tsx +212 -259
  56. package/src/ui/collection_editor/CollectionEditorDialog.tsx +237 -169
  57. package/src/ui/collection_editor/CollectionEditorWelcomeView.tsx +133 -67
  58. package/src/ui/collection_editor/CollectionJsonImportDialog.tsx +171 -0
  59. package/src/ui/collection_editor/CollectionPropertiesEditorForm.tsx +190 -91
  60. package/src/ui/collection_editor/DisplaySettingsForm.tsx +333 -0
  61. package/src/ui/collection_editor/EntityActionsEditTab.tsx +106 -96
  62. package/src/ui/collection_editor/EntityActionsSelectDialog.tsx +6 -7
  63. package/src/ui/collection_editor/EntityCustomViewsSelectDialog.tsx +1 -3
  64. package/src/ui/collection_editor/EnumForm.tsx +147 -100
  65. package/src/ui/collection_editor/ExtendSettingsForm.tsx +93 -0
  66. package/src/ui/collection_editor/GeneralSettingsForm.tsx +337 -0
  67. package/src/ui/collection_editor/GetCodeDialog.tsx +57 -36
  68. package/src/ui/collection_editor/KanbanConfigSection.tsx +207 -0
  69. package/src/ui/collection_editor/LayoutModeSwitch.tsx +22 -41
  70. package/src/ui/collection_editor/PropertyEditView.tsx +206 -142
  71. package/src/ui/collection_editor/PropertyFieldPreview.tsx +1 -1
  72. package/src/ui/collection_editor/PropertyTree.tsx +130 -58
  73. package/src/ui/collection_editor/SubcollectionsEditTab.tsx +171 -162
  74. package/src/ui/collection_editor/UnsavedChangesDialog.tsx +0 -2
  75. package/src/ui/collection_editor/ViewModeSwitch.tsx +41 -0
  76. package/src/ui/collection_editor/properties/BlockPropertyField.tsx +0 -2
  77. package/src/ui/collection_editor/properties/BooleanPropertyField.tsx +1 -0
  78. package/src/ui/collection_editor/properties/DateTimePropertyField.tsx +117 -35
  79. package/src/ui/collection_editor/properties/EnumPropertyField.tsx +28 -21
  80. package/src/ui/collection_editor/properties/MapPropertyField.tsx +0 -2
  81. package/src/ui/collection_editor/properties/MarkdownPropertyField.tsx +115 -39
  82. package/src/ui/collection_editor/properties/StoragePropertyField.tsx +1 -1
  83. package/src/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +861 -0
  84. package/src/ui/collection_editor/properties/conditions/ConditionsPanel.tsx +28 -0
  85. package/src/ui/collection_editor/properties/conditions/EnumConditionsEditor.tsx +599 -0
  86. package/src/ui/collection_editor/properties/conditions/index.ts +6 -0
  87. package/src/ui/collection_editor/properties/conditions/property_paths.ts +92 -0
  88. package/src/ui/collection_editor/properties/validation/ValidationPanel.tsx +1 -1
  89. package/src/useCollectionEditorPlugin.tsx +32 -17
  90. package/src/utils/validateCollectionJson.ts +380 -0
@@ -0,0 +1,92 @@
1
+ import { Properties, Property, PropertyOrBuilder, isPropertyBuilder } from "@firecms/core";
2
+
3
+ /**
4
+ * Recursively extract all property paths from a Properties object.
5
+ * For nested map properties, creates dot-notation paths like "address.city".
6
+ * Skips PropertyBuilder functions (callbacks) as they cannot be statically analyzed.
7
+ *
8
+ * @param properties - The properties object to extract paths from
9
+ * @param prefix - Optional prefix for nested paths (used in recursion)
10
+ * @returns Array of property path strings
11
+ */
12
+ export function getPropertyPaths(
13
+ properties: Properties | undefined,
14
+ prefix: string = ""
15
+ ): string[] {
16
+ if (!properties) return [];
17
+
18
+ const paths: string[] = [];
19
+
20
+ for (const [key, propertyOrBuilder] of Object.entries(properties)) {
21
+ if (!propertyOrBuilder) continue;
22
+
23
+ // Skip PropertyBuilder functions - they require runtime values to resolve
24
+ if (isPropertyBuilder(propertyOrBuilder)) continue;
25
+
26
+ const property = propertyOrBuilder as Property;
27
+ const fullPath = prefix ? `${prefix}.${key}` : key;
28
+ paths.push(fullPath);
29
+
30
+ // Recursively add nested map properties
31
+ if (property.dataType === "map" && property.properties) {
32
+ const nestedPaths = getPropertyPaths(
33
+ property.properties as Properties,
34
+ fullPath
35
+ );
36
+ paths.push(...nestedPaths);
37
+ }
38
+
39
+ // For arrays with object items, add the nested paths too
40
+ if (property.dataType === "array" && property.of) {
41
+ const ofPropertyOrBuilder = property.of as PropertyOrBuilder;
42
+ // Skip if the array's 'of' is a PropertyBuilder
43
+ if (!isPropertyBuilder(ofPropertyOrBuilder)) {
44
+ const ofProperty = ofPropertyOrBuilder as Property;
45
+ if (ofProperty.dataType === "map" && ofProperty.properties) {
46
+ const nestedPaths = getPropertyPaths(
47
+ ofProperty.properties as Properties,
48
+ `${fullPath}[]`
49
+ );
50
+ paths.push(...nestedPaths);
51
+ }
52
+ }
53
+ }
54
+ }
55
+
56
+ return paths;
57
+ }
58
+
59
+ /**
60
+ * Get property paths grouped by top-level property for UI display.
61
+ * Skips PropertyBuilder functions.
62
+ *
63
+ * @param properties - The properties object
64
+ * @returns Object with top-level keys mapping to their nested paths
65
+ */
66
+ export function getGroupedPropertyPaths(
67
+ properties: Properties | undefined
68
+ ): Record<string, string[]> {
69
+ if (!properties) return {};
70
+
71
+ const grouped: Record<string, string[]> = {};
72
+
73
+ for (const [key, propertyOrBuilder] of Object.entries(properties)) {
74
+ if (!propertyOrBuilder) continue;
75
+
76
+ // Skip PropertyBuilder functions
77
+ if (isPropertyBuilder(propertyOrBuilder)) continue;
78
+
79
+ const property = propertyOrBuilder as Property;
80
+ grouped[key] = [key];
81
+
82
+ if (property.dataType === "map" && property.properties) {
83
+ const nestedPaths = getPropertyPaths(
84
+ property.properties as Properties,
85
+ key
86
+ );
87
+ grouped[key].push(...nestedPaths);
88
+ }
89
+ }
90
+
91
+ return grouped;
92
+ }
@@ -12,7 +12,7 @@ export function ValidationPanel({
12
12
  asField={true}
13
13
  innerClassName="p-4"
14
14
  title={
15
- <div className="flex flex-row text-surface-500">
15
+ <div className="flex flex-row text-surface-500 text-text-secondary dark:text-text-secondary-dark">
16
16
  <RuleIcon/>
17
17
  <Typography variant={"subtitle2"}
18
18
  className="ml-4">
@@ -7,6 +7,7 @@ import { HomePageEditorCollectionAction } from "./ui/HomePageEditorCollectionAct
7
7
  import { PersistedCollection } from "./types/persisted_collection";
8
8
  import { CollectionInference } from "./types/collection_inference";
9
9
  import { CollectionsConfigController } from "./types/config_controller";
10
+ import { CollectionGenerationCallback } from "./api/generateCollectionApi";
10
11
  import { CollectionViewHeaderAction } from "./ui/CollectionViewHeaderAction";
11
12
  import { PropertyAddColumnComponent } from "./ui/PropertyAddColumnComponent";
12
13
  import { NewCollectionButton } from "./ui/NewCollectionButton";
@@ -15,6 +16,8 @@ import { useCollectionEditorController } from "./useCollectionEditorController";
15
16
  import { EditorCollectionActionStart } from "./ui/EditorCollectionActionStart";
16
17
  import { NewCollectionCard } from "./ui/NewCollectionCard";
17
18
  import { EditorEntityAction } from "./ui/EditorEntityAction";
19
+ import { KanbanSetupAction } from "./ui/KanbanSetupAction";
20
+ import { AddKanbanColumnAction } from "./ui/AddKanbanColumnAction";
18
21
 
19
22
  export interface CollectionConfigControllerProps<EC extends PersistedCollection = PersistedCollection, USER extends User = User> {
20
23
 
@@ -54,6 +57,12 @@ export interface CollectionConfigControllerProps<EC extends PersistedCollection
54
57
 
55
58
  includeIntroView?: boolean;
56
59
 
60
+ /**
61
+ * Callback function for generating/modifying collections.
62
+ * The plugin is API-agnostic - the consumer provides the implementation.
63
+ */
64
+ generateCollection?: CollectionGenerationCallback;
65
+
57
66
  }
58
67
 
59
68
  /**
@@ -68,18 +77,19 @@ export interface CollectionConfigControllerProps<EC extends PersistedCollection
68
77
  * @param collectionInference
69
78
  */
70
79
  export function useCollectionEditorPlugin<EC extends PersistedCollection = PersistedCollection, USER extends User = User>
71
- ({
72
- collectionConfigController,
73
- configPermissions,
74
- reservedGroups,
75
- extraView,
76
- getUser,
77
- collectionInference,
78
- getData,
79
- onAnalyticsEvent,
80
- includeIntroView = true,
81
- pathSuggestions
82
- }: CollectionConfigControllerProps<EC, USER>): FireCMSPlugin<any, any, PersistedCollection> {
80
+ ({
81
+ collectionConfigController,
82
+ configPermissions,
83
+ reservedGroups,
84
+ extraView,
85
+ getUser,
86
+ collectionInference,
87
+ getData,
88
+ onAnalyticsEvent,
89
+ includeIntroView = true,
90
+ pathSuggestions,
91
+ generateCollection
92
+ }: CollectionConfigControllerProps<EC, USER>): FireCMSPlugin<any, any, PersistedCollection> {
83
93
 
84
94
  return {
85
95
  key: "collection_editor",
@@ -95,12 +105,13 @@ export function useCollectionEditorPlugin<EC extends PersistedCollection = Persi
95
105
  getUser,
96
106
  getData,
97
107
  onAnalyticsEvent,
98
- pathSuggestions
108
+ pathSuggestions,
109
+ generateCollection
99
110
  }
100
111
  },
101
112
  homePage: {
102
- additionalActions: <NewCollectionButton/>,
103
- additionalChildrenStart: includeIntroView ? <IntroWidget/> : undefined,
113
+ additionalActions: <NewCollectionButton />,
114
+ additionalChildrenStart: includeIntroView ? <IntroWidget /> : undefined,
104
115
  CollectionActions: HomePageEditorCollectionAction,
105
116
  AdditionalCards: NewCollectionCard,
106
117
  allowDragAndDrop: true,
@@ -111,7 +122,11 @@ export function useCollectionEditorPlugin<EC extends PersistedCollection = Persi
111
122
  CollectionActionsStart: EditorCollectionActionStart,
112
123
  CollectionActions: EditorCollectionAction,
113
124
  HeaderAction: CollectionViewHeaderAction,
114
- AddColumnComponent: PropertyAddColumnComponent
125
+ AddColumnComponent: PropertyAddColumnComponent,
126
+ onColumnsReorder: collectionConfigController.updatePropertiesOrder,
127
+ onKanbanColumnsReorder: collectionConfigController.updateKanbanColumnsOrder,
128
+ KanbanSetupComponent: KanbanSetupAction,
129
+ AddKanbanColumnComponent: AddKanbanColumnAction
115
130
  },
116
131
  form: {
117
132
  ActionsTop: EditorEntityAction,
@@ -154,7 +169,7 @@ export function IntroWidget() {
154
169
  sourceClick: "new_collection_card"
155
170
  })
156
171
  : undefined}>
157
- <AddIcon/>Create your first collection
172
+ <AddIcon />Create your first collection
158
173
  </Button>}
159
174
  <Typography color={"secondary"}>
160
175
  You can also define collections programmatically.
@@ -0,0 +1,380 @@
1
+ import { EntityCollection } from "@firecms/core";
2
+
3
+ /**
4
+ * Valid dataType values for properties
5
+ */
6
+ const VALID_DATA_TYPES = [
7
+ "string",
8
+ "number",
9
+ "boolean",
10
+ "date",
11
+ "geopoint",
12
+ "reference",
13
+ "array",
14
+ "map"
15
+ ] as const;
16
+
17
+ type DataType = typeof VALID_DATA_TYPES[number];
18
+
19
+ /**
20
+ * Validation error with path and message
21
+ */
22
+ export interface CollectionValidationError {
23
+ path: string;
24
+ message: string;
25
+ }
26
+
27
+ /**
28
+ * Result of collection JSON validation
29
+ */
30
+ export interface CollectionValidationResult {
31
+ valid: boolean;
32
+ errors: CollectionValidationError[];
33
+ collection?: EntityCollection;
34
+ }
35
+
36
+ /**
37
+ * Validates a property object recursively
38
+ */
39
+ function validateProperty(
40
+ property: any,
41
+ path: string,
42
+ errors: CollectionValidationError[]
43
+ ): void {
44
+ if (typeof property !== "object" || property === null) {
45
+ errors.push({
46
+ path,
47
+ message: "Property must be an object"
48
+ });
49
+ return;
50
+ }
51
+
52
+ // Check dataType
53
+ if (!property.dataType) {
54
+ errors.push({
55
+ path: `${path}.dataType`,
56
+ message: "Required field is missing"
57
+ });
58
+ } else if (!VALID_DATA_TYPES.includes(property.dataType)) {
59
+ errors.push({
60
+ path: `${path}.dataType`,
61
+ message: `Invalid value "${property.dataType}", expected one of: ${VALID_DATA_TYPES.join(", ")}`
62
+ });
63
+ }
64
+
65
+ // Validate name if present
66
+ if (property.name !== undefined && typeof property.name !== "string") {
67
+ errors.push({
68
+ path: `${path}.name`,
69
+ message: "Must be a string"
70
+ });
71
+ }
72
+
73
+ // Validate array "of" property
74
+ if (property.dataType === "array") {
75
+ if (property.of) {
76
+ if (Array.isArray(property.of)) {
77
+ property.of.forEach((ofProp: any, index: number) => {
78
+ validateProperty(ofProp, `${path}.of[${index}]`, errors);
79
+ });
80
+ } else {
81
+ validateProperty(property.of, `${path}.of`, errors);
82
+ }
83
+ }
84
+ // oneOf validation
85
+ if (property.oneOf) {
86
+ if (typeof property.oneOf !== "object") {
87
+ errors.push({
88
+ path: `${path}.oneOf`,
89
+ message: "Must be an object"
90
+ });
91
+ } else if (property.oneOf.properties) {
92
+ validateProperties(property.oneOf.properties, `${path}.oneOf.properties`, errors);
93
+ }
94
+ }
95
+ }
96
+
97
+ // Validate map properties
98
+ if (property.dataType === "map" && property.properties) {
99
+ validateProperties(property.properties, `${path}.properties`, errors);
100
+ }
101
+
102
+ // Validate reference path
103
+ if (property.dataType === "reference") {
104
+ if (property.path !== undefined && typeof property.path !== "string") {
105
+ errors.push({
106
+ path: `${path}.path`,
107
+ message: "Must be a string"
108
+ });
109
+ }
110
+ }
111
+
112
+ // Validate storage config for string
113
+ if (property.dataType === "string" && property.storage) {
114
+ if (typeof property.storage !== "object") {
115
+ errors.push({
116
+ path: `${path}.storage`,
117
+ message: "Must be an object"
118
+ });
119
+ }
120
+ }
121
+
122
+ // Validate enumValues if present
123
+ if (property.enumValues !== undefined) {
124
+ if (!Array.isArray(property.enumValues) && typeof property.enumValues !== "object") {
125
+ errors.push({
126
+ path: `${path}.enumValues`,
127
+ message: "Must be an array or object"
128
+ });
129
+ }
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Validates a properties object (collection of property definitions)
135
+ */
136
+ function validateProperties(
137
+ properties: any,
138
+ path: string,
139
+ errors: CollectionValidationError[]
140
+ ): void {
141
+ if (typeof properties !== "object" || properties === null) {
142
+ errors.push({
143
+ path,
144
+ message: "Must be an object"
145
+ });
146
+ return;
147
+ }
148
+
149
+ for (const [key, property] of Object.entries(properties)) {
150
+ validateProperty(property, `${path}.${key}`, errors);
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Validates optional collection fields
156
+ */
157
+ function validateOptionalFields(
158
+ collection: any,
159
+ errors: CollectionValidationError[]
160
+ ): void {
161
+ // String fields
162
+ const stringFields = [
163
+ "singularName",
164
+ "description",
165
+ "group",
166
+ "databaseId"
167
+ ];
168
+ for (const field of stringFields) {
169
+ if (collection[field] !== undefined && typeof collection[field] !== "string") {
170
+ errors.push({
171
+ path: field,
172
+ message: "Must be a string"
173
+ });
174
+ }
175
+ }
176
+
177
+ // Boolean fields
178
+ const booleanFields = [
179
+ "collectionGroup",
180
+ "textSearchEnabled",
181
+ "selectionEnabled",
182
+ "inlineEditing",
183
+ "hideFromNavigation",
184
+ "hideIdFromForm",
185
+ "hideIdFromCollection",
186
+ "formAutoSave",
187
+ "editable",
188
+ "alwaysApplyDefaultValues",
189
+ "includeJsonView",
190
+ "history"
191
+ ];
192
+ for (const field of booleanFields) {
193
+ if (collection[field] !== undefined && typeof collection[field] !== "boolean") {
194
+ errors.push({
195
+ path: field,
196
+ message: "Must be a boolean"
197
+ });
198
+ }
199
+ }
200
+
201
+ // Icon can be string or object (React node)
202
+ if (collection.icon !== undefined &&
203
+ typeof collection.icon !== "string" &&
204
+ typeof collection.icon !== "object") {
205
+ errors.push({
206
+ path: "icon",
207
+ message: "Must be a string (icon key) or object"
208
+ });
209
+ }
210
+
211
+ // propertiesOrder must be array of strings
212
+ if (collection.propertiesOrder !== undefined) {
213
+ if (!Array.isArray(collection.propertiesOrder)) {
214
+ errors.push({
215
+ path: "propertiesOrder",
216
+ message: "Must be an array of strings"
217
+ });
218
+ } else if (!collection.propertiesOrder.every((item: any) => typeof item === "string")) {
219
+ errors.push({
220
+ path: "propertiesOrder",
221
+ message: "All items must be strings"
222
+ });
223
+ }
224
+ }
225
+
226
+ // subcollections must be array
227
+ if (collection.subcollections !== undefined) {
228
+ if (!Array.isArray(collection.subcollections)) {
229
+ errors.push({
230
+ path: "subcollections",
231
+ message: "Must be an array"
232
+ });
233
+ } else {
234
+ collection.subcollections.forEach((sub: any, index: number) => {
235
+ const subErrors: CollectionValidationError[] = [];
236
+ validateCollectionObject(sub, subErrors);
237
+ subErrors.forEach(err => {
238
+ errors.push({
239
+ path: `subcollections[${index}].${err.path}`,
240
+ message: err.message
241
+ });
242
+ });
243
+ });
244
+ }
245
+ }
246
+
247
+ // defaultViewMode validation
248
+ const validViewModes = ["table", "cards", "kanban"];
249
+ if (collection.defaultViewMode !== undefined) {
250
+ if (!validViewModes.includes(collection.defaultViewMode)) {
251
+ errors.push({
252
+ path: "defaultViewMode",
253
+ message: `Invalid value, expected one of: ${validViewModes.join(", ")}`
254
+ });
255
+ }
256
+ }
257
+
258
+ // kanban config validation
259
+ if (collection.kanban !== undefined) {
260
+ if (typeof collection.kanban !== "object" || collection.kanban === null) {
261
+ errors.push({
262
+ path: "kanban",
263
+ message: "Must be an object"
264
+ });
265
+ } else if (collection.kanban.columnProperty !== undefined &&
266
+ typeof collection.kanban.columnProperty !== "string") {
267
+ errors.push({
268
+ path: "kanban.columnProperty",
269
+ message: "Must be a string"
270
+ });
271
+ }
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Validates a collection object
277
+ */
278
+ function validateCollectionObject(
279
+ collection: any,
280
+ errors: CollectionValidationError[]
281
+ ): void {
282
+ // Required fields
283
+ if (!collection.id) {
284
+ errors.push({
285
+ path: "id",
286
+ message: "Required field is missing"
287
+ });
288
+ } else if (typeof collection.id !== "string") {
289
+ errors.push({
290
+ path: "id",
291
+ message: "Must be a string"
292
+ });
293
+ }
294
+
295
+ if (!collection.name) {
296
+ errors.push({
297
+ path: "name",
298
+ message: "Required field is missing"
299
+ });
300
+ } else if (typeof collection.name !== "string") {
301
+ errors.push({
302
+ path: "name",
303
+ message: "Must be a string"
304
+ });
305
+ }
306
+
307
+ if (!collection.path) {
308
+ errors.push({
309
+ path: "path",
310
+ message: "Required field is missing"
311
+ });
312
+ } else if (typeof collection.path !== "string") {
313
+ errors.push({
314
+ path: "path",
315
+ message: "Must be a string"
316
+ });
317
+ }
318
+
319
+ // Properties validation
320
+ if (collection.properties !== undefined) {
321
+ validateProperties(collection.properties, "properties", errors);
322
+ }
323
+
324
+ // Optional fields
325
+ validateOptionalFields(collection, errors);
326
+ }
327
+
328
+ /**
329
+ * Validates a JSON string representing a collection configuration.
330
+ * Returns detailed validation errors if the JSON is invalid or doesn't match
331
+ * the expected collection schema.
332
+ */
333
+ export function validateCollectionJson(jsonString: string): CollectionValidationResult {
334
+ const errors: CollectionValidationError[] = [];
335
+
336
+ // Try to parse JSON
337
+ let parsed: any;
338
+ try {
339
+ parsed = JSON.parse(jsonString);
340
+ } catch (e: any) {
341
+ // Try to extract line/column info from the error
342
+ const match = e.message.match(/position (\d+)/);
343
+ let message = "Invalid JSON syntax";
344
+ if (match) {
345
+ const position = parseInt(match[1], 10);
346
+ const lines = jsonString.substring(0, position).split("\n");
347
+ const line = lines.length;
348
+ const column = lines[lines.length - 1].length + 1;
349
+ message = `Invalid JSON syntax at line ${line}, column ${column}: ${e.message}`;
350
+ } else {
351
+ message = `Invalid JSON syntax: ${e.message}`;
352
+ }
353
+ return {
354
+ valid: false,
355
+ errors: [{
356
+ path: "",
357
+ message
358
+ }]
359
+ };
360
+ }
361
+
362
+ // Validate collection structure
363
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
364
+ return {
365
+ valid: false,
366
+ errors: [{
367
+ path: "",
368
+ message: "Collection must be an object"
369
+ }]
370
+ };
371
+ }
372
+
373
+ validateCollectionObject(parsed, errors);
374
+
375
+ return {
376
+ valid: errors.length === 0,
377
+ errors,
378
+ collection: errors.length === 0 ? parsed as EntityCollection : undefined
379
+ };
380
+ }