@imjp/writenex-astro 0.1.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.
- package/README.md +539 -0
- package/dist/chunk-5PM6EQE5.js +151 -0
- package/dist/chunk-5PM6EQE5.js.map +1 -0
- package/dist/chunk-7XU5X6CW.js +1331 -0
- package/dist/chunk-7XU5X6CW.js.map +1 -0
- package/dist/chunk-AAOQHQPU.js +574 -0
- package/dist/chunk-AAOQHQPU.js.map +1 -0
- package/dist/chunk-CF2XXJFF.js +1410 -0
- package/dist/chunk-CF2XXJFF.js.map +1 -0
- package/dist/chunk-CRPZUUDU.js +52 -0
- package/dist/chunk-CRPZUUDU.js.map +1 -0
- package/dist/chunk-CYLDJ3HZ.js +310 -0
- package/dist/chunk-CYLDJ3HZ.js.map +1 -0
- package/dist/chunk-KIKIPIFA.js +1 -0
- package/dist/chunk-KIKIPIFA.js.map +1 -0
- package/dist/chunk-XNTQTTJU.js +145 -0
- package/dist/chunk-XNTQTTJU.js.map +1 -0
- package/dist/client/index.css +2 -0
- package/dist/client/index.css.map +1 -0
- package/dist/client/index.js +375 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/styles.css +584 -0
- package/dist/client/variables.css +304 -0
- package/dist/config/index.d.ts +54 -0
- package/dist/config/index.js +38 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config-BmEdBDo_.d.ts +220 -0
- package/dist/content-BWR52vD-.d.ts +64 -0
- package/dist/discovery/index.d.ts +310 -0
- package/dist/discovery/index.js +38 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/errors-C0iYiDTv.d.ts +107 -0
- package/dist/filesystem/index.d.ts +1292 -0
- package/dist/filesystem/index.js +203 -0
- package/dist/filesystem/index.js.map +1 -0
- package/dist/image-FP7w5ZIs.d.ts +47 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +151 -0
- package/dist/index.js.map +1 -0
- package/dist/loader-55LWCXHA.js +12 -0
- package/dist/loader-55LWCXHA.js.map +1 -0
- package/dist/loader-CrdnaAWR.d.ts +327 -0
- package/dist/server/index.d.ts +357 -0
- package/dist/server/index.js +37 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +94 -0
- package/src/client/App.tsx +900 -0
- package/src/client/components/ConfigPanel/ConfigPanel.css +553 -0
- package/src/client/components/ConfigPanel/ConfigPanel.tsx +396 -0
- package/src/client/components/ConfigPanel/index.ts +6 -0
- package/src/client/components/CreateContentModal/CreateContentModal.css +327 -0
- package/src/client/components/CreateContentModal/CreateContentModal.tsx +216 -0
- package/src/client/components/CreateContentModal/index.ts +7 -0
- package/src/client/components/Editor/Editor.css +885 -0
- package/src/client/components/Editor/Editor.tsx +484 -0
- package/src/client/components/Editor/ImageDialog.css +344 -0
- package/src/client/components/Editor/ImageDialog.tsx +367 -0
- package/src/client/components/Editor/LinkDialog.css +326 -0
- package/src/client/components/Editor/LinkDialog.tsx +332 -0
- package/src/client/components/Editor/index.ts +6 -0
- package/src/client/components/FrontmatterForm/FrontmatterForm.css +468 -0
- package/src/client/components/FrontmatterForm/FrontmatterForm.tsx +914 -0
- package/src/client/components/FrontmatterForm/index.ts +7 -0
- package/src/client/components/Header/Header.css +300 -0
- package/src/client/components/Header/Header.tsx +300 -0
- package/src/client/components/Header/index.ts +7 -0
- package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.css +239 -0
- package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.tsx +151 -0
- package/src/client/components/KeyboardShortcuts/index.ts +6 -0
- package/src/client/components/LazyEditor.tsx +75 -0
- package/src/client/components/LiveRegion/LiveRegion.css +19 -0
- package/src/client/components/LiveRegion/LiveRegion.tsx +60 -0
- package/src/client/components/LiveRegion/index.ts +7 -0
- package/src/client/components/SearchReplace/SearchReplacePanel.css +300 -0
- package/src/client/components/SearchReplace/SearchReplacePanel.tsx +332 -0
- package/src/client/components/SearchReplace/index.ts +7 -0
- package/src/client/components/SelectCollectionModal/SelectCollectionModal.css +308 -0
- package/src/client/components/SelectCollectionModal/SelectCollectionModal.tsx +223 -0
- package/src/client/components/SelectCollectionModal/index.ts +7 -0
- package/src/client/components/Sidebar/Sidebar.css +570 -0
- package/src/client/components/Sidebar/Sidebar.tsx +617 -0
- package/src/client/components/Sidebar/index.ts +7 -0
- package/src/client/components/SkipLink/SkipLink.css +51 -0
- package/src/client/components/SkipLink/SkipLink.tsx +67 -0
- package/src/client/components/SkipLink/index.ts +7 -0
- package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.css +233 -0
- package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.tsx +160 -0
- package/src/client/components/UnsavedChangesModal/index.ts +1 -0
- package/src/client/components/VersionHistory/DiffViewer.css +430 -0
- package/src/client/components/VersionHistory/DiffViewer.tsx +383 -0
- package/src/client/components/VersionHistory/VersionActions.css +318 -0
- package/src/client/components/VersionHistory/VersionActions.tsx +277 -0
- package/src/client/components/VersionHistory/VersionHistoryPanel.css +369 -0
- package/src/client/components/VersionHistory/VersionHistoryPanel.tsx +469 -0
- package/src/client/components/VersionHistory/index.ts +9 -0
- package/src/client/context/ApiContext.tsx +154 -0
- package/src/client/context/ThemeContext.tsx +172 -0
- package/src/client/hooks/useAnnounce.ts +201 -0
- package/src/client/hooks/useApi.ts +374 -0
- package/src/client/hooks/useArrowNavigation.ts +286 -0
- package/src/client/hooks/useAutosave.ts +241 -0
- package/src/client/hooks/useFocusTrap.ts +178 -0
- package/src/client/hooks/useKeyboardShortcuts.ts +203 -0
- package/src/client/hooks/useSearch.ts +206 -0
- package/src/client/hooks/useVersionHistory.ts +451 -0
- package/src/client/index.tsx +70 -0
- package/src/client/styles.css +584 -0
- package/src/client/utils/focus.ts +57 -0
- package/src/client/utils/openInEditor.ts +130 -0
- package/src/client/variables.css +304 -0
- package/src/config/defaults.ts +109 -0
- package/src/config/index.ts +32 -0
- package/src/config/loader.ts +174 -0
- package/src/config/schema.ts +161 -0
- package/src/core/constants.ts +39 -0
- package/src/core/errors.ts +739 -0
- package/src/core/index.ts +11 -0
- package/src/discovery/collections.ts +216 -0
- package/src/discovery/index.ts +33 -0
- package/src/discovery/patterns.ts +702 -0
- package/src/discovery/schema.ts +453 -0
- package/src/filesystem/images.ts +798 -0
- package/src/filesystem/index.ts +107 -0
- package/src/filesystem/reader.ts +452 -0
- package/src/filesystem/version-config.ts +390 -0
- package/src/filesystem/versions.ts +1339 -0
- package/src/filesystem/watcher.ts +226 -0
- package/src/filesystem/writer.ts +540 -0
- package/src/index.ts +61 -0
- package/src/integration.ts +228 -0
- package/src/server/assets.ts +254 -0
- package/src/server/cache.ts +355 -0
- package/src/server/index.ts +33 -0
- package/src/server/middleware.ts +209 -0
- package/src/server/routes.ts +1428 -0
- package/src/types/api.ts +61 -0
- package/src/types/config.ts +134 -0
- package/src/types/content.ts +64 -0
- package/src/types/image.ts +48 -0
- package/src/types/index.ts +58 -0
- package/src/types/version.ts +117 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Schema auto-detection for content collections
|
|
3
|
+
*
|
|
4
|
+
* This module analyzes frontmatter from sample content files to automatically
|
|
5
|
+
* infer the schema (field types, required status, enums, etc.).
|
|
6
|
+
*
|
|
7
|
+
* ## Detection Process:
|
|
8
|
+
* 1. Read sample files from the collection (up to 20)
|
|
9
|
+
* 2. Parse frontmatter from each file
|
|
10
|
+
* 3. Analyze field patterns across all samples
|
|
11
|
+
* 4. Infer field types and constraints
|
|
12
|
+
* 5. Generate schema definition
|
|
13
|
+
*
|
|
14
|
+
* ## Detected Types:
|
|
15
|
+
* - string: Plain text values
|
|
16
|
+
* - number: Numeric values
|
|
17
|
+
* - boolean: True/false values
|
|
18
|
+
* - date: ISO date strings or Date objects
|
|
19
|
+
* - array: Arrays (with item type detection)
|
|
20
|
+
* - image: Paths ending with image extensions
|
|
21
|
+
*
|
|
22
|
+
* @module @writenex/astro/discovery/schema
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { CollectionSchema, FieldType, SchemaField } from "@/types";
|
|
26
|
+
import { readCollection } from "@/filesystem/reader";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Maximum number of files to sample for schema detection
|
|
30
|
+
*/
|
|
31
|
+
const MAX_SAMPLE_FILES = 20;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Minimum presence ratio to consider a field required
|
|
35
|
+
* (field must appear in at least 90% of files)
|
|
36
|
+
*/
|
|
37
|
+
const REQUIRED_THRESHOLD = 0.9;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Maximum unique values to consider a field as enum
|
|
41
|
+
*/
|
|
42
|
+
const ENUM_MAX_VALUES = 10;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Minimum ratio of unique values to total to NOT be an enum
|
|
46
|
+
* (if uniqueValues / total < 0.3, it's likely an enum)
|
|
47
|
+
*/
|
|
48
|
+
const ENUM_RATIO_THRESHOLD = 0.3;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Image file extensions for detection
|
|
52
|
+
*/
|
|
53
|
+
const IMAGE_EXTENSIONS = [
|
|
54
|
+
".jpg",
|
|
55
|
+
".jpeg",
|
|
56
|
+
".png",
|
|
57
|
+
".gif",
|
|
58
|
+
".webp",
|
|
59
|
+
".avif",
|
|
60
|
+
".svg",
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Field analysis data collected from samples
|
|
65
|
+
*/
|
|
66
|
+
interface FieldAnalysis {
|
|
67
|
+
/** Number of files where this field appears */
|
|
68
|
+
presentCount: number;
|
|
69
|
+
/** Detected types for this field across samples */
|
|
70
|
+
types: Set<string>;
|
|
71
|
+
/** Sample values for enum detection */
|
|
72
|
+
values: unknown[];
|
|
73
|
+
/** Whether values look like image paths */
|
|
74
|
+
hasImagePaths: boolean;
|
|
75
|
+
/** Whether values look like dates */
|
|
76
|
+
hasDateValues: boolean;
|
|
77
|
+
/** For arrays, analysis of item types */
|
|
78
|
+
arrayItemTypes: Set<string>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Result of schema detection
|
|
83
|
+
*/
|
|
84
|
+
export interface SchemaDetectionResult {
|
|
85
|
+
/** The detected schema */
|
|
86
|
+
schema: CollectionSchema;
|
|
87
|
+
/** Number of files analyzed */
|
|
88
|
+
samplesAnalyzed: number;
|
|
89
|
+
/** Confidence score (0-1) based on sample consistency */
|
|
90
|
+
confidence: number;
|
|
91
|
+
/** Fields that had inconsistent types across samples */
|
|
92
|
+
warnings: string[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if a string looks like an image path
|
|
97
|
+
*
|
|
98
|
+
* @param value - Value to check
|
|
99
|
+
* @returns True if it looks like an image path
|
|
100
|
+
*/
|
|
101
|
+
function isImagePath(value: unknown): boolean {
|
|
102
|
+
if (typeof value !== "string") return false;
|
|
103
|
+
|
|
104
|
+
const lowered = value.toLowerCase();
|
|
105
|
+
return IMAGE_EXTENSIONS.some((ext) => lowered.endsWith(ext));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if a value looks like a date
|
|
110
|
+
*
|
|
111
|
+
* @param value - Value to check
|
|
112
|
+
* @returns True if it looks like a date
|
|
113
|
+
*/
|
|
114
|
+
function isDateValue(value: unknown): boolean {
|
|
115
|
+
// Already a Date object
|
|
116
|
+
if (value instanceof Date) return true;
|
|
117
|
+
|
|
118
|
+
// ISO date string (YYYY-MM-DD or full ISO)
|
|
119
|
+
if (typeof value === "string") {
|
|
120
|
+
// Full ISO format
|
|
121
|
+
if (/^\d{4}-\d{2}-\d{2}(T|\s)/.test(value)) return true;
|
|
122
|
+
// Simple date format
|
|
123
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Detect the JavaScript type of a value
|
|
131
|
+
*
|
|
132
|
+
* @param value - Value to analyze
|
|
133
|
+
* @returns Detected type string
|
|
134
|
+
*/
|
|
135
|
+
function detectValueType(value: unknown): string {
|
|
136
|
+
if (value === null || value === undefined) return "null";
|
|
137
|
+
if (typeof value === "boolean") return "boolean";
|
|
138
|
+
if (typeof value === "number") return "number";
|
|
139
|
+
if (typeof value === "string") return "string";
|
|
140
|
+
if (Array.isArray(value)) return "array";
|
|
141
|
+
if (value instanceof Date) return "date";
|
|
142
|
+
if (typeof value === "object") return "object";
|
|
143
|
+
return "unknown";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Convert detected type to schema field type
|
|
148
|
+
*
|
|
149
|
+
* @param analysis - Field analysis data
|
|
150
|
+
* @returns The appropriate FieldType
|
|
151
|
+
*/
|
|
152
|
+
function inferFieldType(analysis: FieldAnalysis): FieldType {
|
|
153
|
+
// If it has image paths, it's an image field
|
|
154
|
+
if (analysis.hasImagePaths) return "image";
|
|
155
|
+
|
|
156
|
+
// If it has date values, it's a date field
|
|
157
|
+
if (analysis.hasDateValues) return "date";
|
|
158
|
+
|
|
159
|
+
// Check detected types (excluding null)
|
|
160
|
+
const nonNullTypes = new Set([...analysis.types].filter((t) => t !== "null"));
|
|
161
|
+
|
|
162
|
+
// Single type is easy
|
|
163
|
+
if (nonNullTypes.size === 1) {
|
|
164
|
+
const type = [...nonNullTypes][0];
|
|
165
|
+
switch (type) {
|
|
166
|
+
case "boolean":
|
|
167
|
+
return "boolean";
|
|
168
|
+
case "number":
|
|
169
|
+
return "number";
|
|
170
|
+
case "array":
|
|
171
|
+
return "array";
|
|
172
|
+
case "object":
|
|
173
|
+
return "object";
|
|
174
|
+
case "date":
|
|
175
|
+
return "date";
|
|
176
|
+
default:
|
|
177
|
+
return "string";
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Mixed types - default to string (most flexible)
|
|
182
|
+
return "string";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Detect if a field should be treated as an enum
|
|
187
|
+
*
|
|
188
|
+
* @param values - All values seen for this field
|
|
189
|
+
* @param totalSamples - Total number of samples
|
|
190
|
+
* @returns Array of enum values, or undefined if not an enum
|
|
191
|
+
*/
|
|
192
|
+
function detectEnum(
|
|
193
|
+
values: unknown[],
|
|
194
|
+
totalSamples: number
|
|
195
|
+
): string[] | undefined {
|
|
196
|
+
// Filter to string values only
|
|
197
|
+
const stringValues = values.filter(
|
|
198
|
+
(v): v is string => typeof v === "string" && v.length > 0
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
if (stringValues.length === 0) return undefined;
|
|
202
|
+
|
|
203
|
+
// Get unique values
|
|
204
|
+
const uniqueValues = [...new Set(stringValues)];
|
|
205
|
+
|
|
206
|
+
// Check if it's a good candidate for enum
|
|
207
|
+
if (uniqueValues.length > ENUM_MAX_VALUES) return undefined;
|
|
208
|
+
|
|
209
|
+
// Check ratio of unique to total
|
|
210
|
+
const ratio = uniqueValues.length / totalSamples;
|
|
211
|
+
if (ratio > ENUM_RATIO_THRESHOLD) return undefined;
|
|
212
|
+
|
|
213
|
+
// Must have at least 2 unique values and appear multiple times
|
|
214
|
+
if (uniqueValues.length < 2) return undefined;
|
|
215
|
+
if (stringValues.length < totalSamples * 0.5) return undefined;
|
|
216
|
+
|
|
217
|
+
return uniqueValues.sort();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Detect item type for array fields
|
|
222
|
+
*
|
|
223
|
+
* @param analysis - Field analysis data
|
|
224
|
+
* @returns The detected item type, or undefined
|
|
225
|
+
*/
|
|
226
|
+
function detectArrayItemType(analysis: FieldAnalysis): string | undefined {
|
|
227
|
+
if (analysis.arrayItemTypes.size === 0) return undefined;
|
|
228
|
+
|
|
229
|
+
// Filter out null
|
|
230
|
+
const types = [...analysis.arrayItemTypes].filter((t) => t !== "null");
|
|
231
|
+
|
|
232
|
+
if (types.length === 0) return undefined;
|
|
233
|
+
if (types.length === 1) return types[0];
|
|
234
|
+
|
|
235
|
+
// Mixed types - default to string
|
|
236
|
+
return "string";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Analyze frontmatter from content items to detect schema
|
|
241
|
+
*
|
|
242
|
+
* @param collectionPath - Absolute path to the collection directory
|
|
243
|
+
* @returns Schema detection result
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```typescript
|
|
247
|
+
* const result = await detectSchema('/project/src/content/blog');
|
|
248
|
+
* console.log(result.schema);
|
|
249
|
+
* // {
|
|
250
|
+
* // title: { type: 'string', required: true },
|
|
251
|
+
* // pubDate: { type: 'date', required: true },
|
|
252
|
+
* // draft: { type: 'boolean', required: false, default: false },
|
|
253
|
+
* // tags: { type: 'array', required: false, items: 'string' },
|
|
254
|
+
* // }
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
export async function detectSchema(
|
|
258
|
+
collectionPath: string
|
|
259
|
+
): Promise<SchemaDetectionResult> {
|
|
260
|
+
const warnings: string[] = [];
|
|
261
|
+
|
|
262
|
+
// Read sample content files
|
|
263
|
+
const items = await readCollection(collectionPath, {
|
|
264
|
+
includeDrafts: true,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Limit to max samples
|
|
268
|
+
const samples = items.slice(0, MAX_SAMPLE_FILES);
|
|
269
|
+
|
|
270
|
+
if (samples.length === 0) {
|
|
271
|
+
return {
|
|
272
|
+
schema: {},
|
|
273
|
+
samplesAnalyzed: 0,
|
|
274
|
+
confidence: 0,
|
|
275
|
+
warnings: ["No content files found in collection"],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Analyze each field across all samples
|
|
280
|
+
const fieldAnalyses = new Map<string, FieldAnalysis>();
|
|
281
|
+
|
|
282
|
+
for (const item of samples) {
|
|
283
|
+
for (const [fieldName, value] of Object.entries(item.frontmatter)) {
|
|
284
|
+
// Get or create field analysis
|
|
285
|
+
let analysis = fieldAnalyses.get(fieldName);
|
|
286
|
+
if (!analysis) {
|
|
287
|
+
analysis = {
|
|
288
|
+
presentCount: 0,
|
|
289
|
+
types: new Set(),
|
|
290
|
+
values: [],
|
|
291
|
+
hasImagePaths: false,
|
|
292
|
+
hasDateValues: false,
|
|
293
|
+
arrayItemTypes: new Set(),
|
|
294
|
+
};
|
|
295
|
+
fieldAnalyses.set(fieldName, analysis);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Update analysis
|
|
299
|
+
analysis.presentCount++;
|
|
300
|
+
analysis.types.add(detectValueType(value));
|
|
301
|
+
analysis.values.push(value);
|
|
302
|
+
|
|
303
|
+
// Check for special types
|
|
304
|
+
if (isImagePath(value)) {
|
|
305
|
+
analysis.hasImagePaths = true;
|
|
306
|
+
}
|
|
307
|
+
if (isDateValue(value)) {
|
|
308
|
+
analysis.hasDateValues = true;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Analyze array items
|
|
312
|
+
if (Array.isArray(value)) {
|
|
313
|
+
for (const item of value) {
|
|
314
|
+
analysis.arrayItemTypes.add(detectValueType(item));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Generate schema from analysis
|
|
321
|
+
const schema: CollectionSchema = {};
|
|
322
|
+
const totalSamples = samples.length;
|
|
323
|
+
|
|
324
|
+
for (const [fieldName, analysis] of fieldAnalyses) {
|
|
325
|
+
const fieldType = inferFieldType(analysis);
|
|
326
|
+
const isRequired =
|
|
327
|
+
analysis.presentCount / totalSamples >= REQUIRED_THRESHOLD;
|
|
328
|
+
|
|
329
|
+
const field: SchemaField = {
|
|
330
|
+
type: fieldType,
|
|
331
|
+
required: isRequired,
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// Add array item type if applicable
|
|
335
|
+
if (fieldType === "array") {
|
|
336
|
+
const itemType = detectArrayItemType(analysis);
|
|
337
|
+
if (itemType) {
|
|
338
|
+
field.items = itemType;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Detect enum for string fields
|
|
343
|
+
if (fieldType === "string") {
|
|
344
|
+
const enumValues = detectEnum(analysis.values, totalSamples);
|
|
345
|
+
if (enumValues) {
|
|
346
|
+
// Store enum values in the field
|
|
347
|
+
// Note: We use 'default' to store enum options since SchemaField
|
|
348
|
+
// doesn't have an 'enum' property - this can be enhanced later
|
|
349
|
+
field.description = `Options: ${enumValues.join(", ")}`;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Detect default value for boolean fields
|
|
354
|
+
if (fieldType === "boolean") {
|
|
355
|
+
const boolValues = analysis.values.filter(
|
|
356
|
+
(v): v is boolean => typeof v === "boolean"
|
|
357
|
+
);
|
|
358
|
+
if (boolValues.length > 0) {
|
|
359
|
+
// Use most common value as default
|
|
360
|
+
const trueCount = boolValues.filter((v) => v === true).length;
|
|
361
|
+
const falseCount = boolValues.filter((v) => v === false).length;
|
|
362
|
+
field.default = trueCount > falseCount ? true : false;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Check for type inconsistencies
|
|
367
|
+
const nonNullTypes = [...analysis.types].filter((t) => t !== "null");
|
|
368
|
+
if (nonNullTypes.length > 1) {
|
|
369
|
+
warnings.push(
|
|
370
|
+
`Field "${fieldName}" has inconsistent types: ${nonNullTypes.join(", ")}`
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
schema[fieldName] = field;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Calculate confidence based on consistency
|
|
378
|
+
const inconsistentFields = warnings.filter((w) =>
|
|
379
|
+
w.includes("inconsistent")
|
|
380
|
+
).length;
|
|
381
|
+
const confidence = Math.max(
|
|
382
|
+
0,
|
|
383
|
+
1 - inconsistentFields / Math.max(1, fieldAnalyses.size)
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
schema,
|
|
388
|
+
samplesAnalyzed: totalSamples,
|
|
389
|
+
confidence,
|
|
390
|
+
warnings,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Merge detected schema with user-provided schema
|
|
396
|
+
*
|
|
397
|
+
* User schema takes precedence over detected schema.
|
|
398
|
+
*
|
|
399
|
+
* @param detected - Auto-detected schema
|
|
400
|
+
* @param userSchema - User-provided schema overrides
|
|
401
|
+
* @returns Merged schema
|
|
402
|
+
*/
|
|
403
|
+
export function mergeSchema(
|
|
404
|
+
detected: CollectionSchema,
|
|
405
|
+
userSchema?: CollectionSchema
|
|
406
|
+
): CollectionSchema {
|
|
407
|
+
if (!userSchema) return detected;
|
|
408
|
+
|
|
409
|
+
const merged: CollectionSchema = { ...detected };
|
|
410
|
+
|
|
411
|
+
for (const [fieldName, userField] of Object.entries(userSchema)) {
|
|
412
|
+
merged[fieldName] = {
|
|
413
|
+
...detected[fieldName],
|
|
414
|
+
...userField,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return merged;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Convert schema to a human-readable description
|
|
423
|
+
*
|
|
424
|
+
* @param schema - The schema to describe
|
|
425
|
+
* @returns Human-readable description
|
|
426
|
+
*/
|
|
427
|
+
export function describeSchema(schema: CollectionSchema): string {
|
|
428
|
+
const lines: string[] = [];
|
|
429
|
+
|
|
430
|
+
for (const [fieldName, field] of Object.entries(schema)) {
|
|
431
|
+
let desc = `- ${fieldName}: ${field.type}`;
|
|
432
|
+
|
|
433
|
+
if (field.required) {
|
|
434
|
+
desc += " (required)";
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (field.items) {
|
|
438
|
+
desc += ` of ${field.items}`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (field.default !== undefined) {
|
|
442
|
+
desc += ` [default: ${JSON.stringify(field.default)}]`;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (field.description) {
|
|
446
|
+
desc += ` - ${field.description}`;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
lines.push(desc);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return lines.join("\n");
|
|
453
|
+
}
|