@happyvertical/smrt-svelte 0.55.7 → 0.55.9

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 (197) hide show
  1. package/AGENTS.md +7 -44
  2. package/README.md +112 -1
  3. package/agents/browser-ai.md +47 -0
  4. package/agents/command-palette.md +85 -0
  5. package/agents/import-export.md +62 -0
  6. package/agents/overview-surfaces.md +255 -0
  7. package/agents/screens.md +57 -0
  8. package/dist/browser-ai/adapters/tts/browser-synthesis.d.ts +5 -0
  9. package/dist/browser-ai/adapters/tts/browser-synthesis.d.ts.map +1 -1
  10. package/dist/browser-ai/adapters/tts/browser-synthesis.js +83 -55
  11. package/dist/components/command-palette/CommandPalette.svelte +75 -0
  12. package/dist/components/command-palette/CommandPalette.svelte.d.ts +22 -0
  13. package/dist/components/command-palette/CommandPalette.svelte.d.ts.map +1 -0
  14. package/dist/components/command-palette/CommandPaletteDialog.svelte +406 -0
  15. package/dist/components/command-palette/CommandPaletteDialog.svelte.d.ts +18 -0
  16. package/dist/components/command-palette/CommandPaletteDialog.svelte.d.ts.map +1 -0
  17. package/dist/components/command-palette/CommandPaletteTrigger.svelte +91 -0
  18. package/dist/components/command-palette/CommandPaletteTrigger.svelte.d.ts +17 -0
  19. package/dist/components/command-palette/CommandPaletteTrigger.svelte.d.ts.map +1 -0
  20. package/dist/components/command-palette/context.d.ts +18 -0
  21. package/dist/components/command-palette/context.d.ts.map +1 -0
  22. package/dist/components/command-palette/context.js +38 -0
  23. package/dist/components/command-palette/controller.svelte.d.ts +70 -0
  24. package/dist/components/command-palette/controller.svelte.d.ts.map +1 -0
  25. package/dist/components/command-palette/controller.svelte.js +379 -0
  26. package/dist/components/command-palette/hotkeys.d.ts +30 -0
  27. package/dist/components/command-palette/hotkeys.d.ts.map +1 -0
  28. package/dist/components/command-palette/hotkeys.js +161 -0
  29. package/dist/components/command-palette/index.d.ts +19 -0
  30. package/dist/components/command-palette/index.d.ts.map +1 -0
  31. package/dist/components/command-palette/index.js +17 -0
  32. package/dist/components/command-palette/match.d.ts +38 -0
  33. package/dist/components/command-palette/match.d.ts.map +1 -0
  34. package/dist/components/command-palette/match.js +172 -0
  35. package/dist/components/command-palette/providers.d.ts +159 -0
  36. package/dist/components/command-palette/providers.d.ts.map +1 -0
  37. package/dist/components/command-palette/providers.js +345 -0
  38. package/dist/components/command-palette/rank.d.ts +20 -0
  39. package/dist/components/command-palette/rank.d.ts.map +1 -0
  40. package/dist/components/command-palette/rank.js +74 -0
  41. package/dist/components/command-palette/types.d.ts +119 -0
  42. package/dist/components/command-palette/types.d.ts.map +1 -0
  43. package/dist/components/command-palette/types.js +10 -0
  44. package/dist/components/command-palette/use-provider.svelte.d.ts +14 -0
  45. package/dist/components/command-palette/use-provider.svelte.d.ts.map +1 -0
  46. package/dist/components/command-palette/use-provider.svelte.js +23 -0
  47. package/dist/components/import-export/ImportExport.svelte +678 -0
  48. package/dist/components/import-export/ImportExport.svelte.d.ts +5 -0
  49. package/dist/components/import-export/ImportExport.svelte.d.ts.map +1 -0
  50. package/dist/components/import-export/collection-adapter.d.ts +35 -0
  51. package/dist/components/import-export/collection-adapter.d.ts.map +1 -0
  52. package/dist/components/import-export/collection-adapter.js +42 -0
  53. package/dist/components/import-export/csv.d.ts +60 -0
  54. package/dist/components/import-export/csv.d.ts.map +1 -0
  55. package/dist/components/import-export/csv.js +192 -0
  56. package/dist/components/import-export/download.d.ts +4 -0
  57. package/dist/components/import-export/download.d.ts.map +1 -0
  58. package/dist/components/import-export/download.js +17 -0
  59. package/dist/components/import-export/export.d.ts +25 -0
  60. package/dist/components/import-export/export.d.ts.map +1 -0
  61. package/dist/components/import-export/export.js +68 -0
  62. package/dist/components/import-export/fields.d.ts +48 -0
  63. package/dist/components/import-export/fields.d.ts.map +1 -0
  64. package/dist/components/import-export/fields.js +103 -0
  65. package/dist/components/import-export/index.d.ts +12 -0
  66. package/dist/components/import-export/index.d.ts.map +1 -0
  67. package/dist/components/import-export/index.js +9 -0
  68. package/dist/components/import-export/mapping.d.ts +12 -0
  69. package/dist/components/import-export/mapping.d.ts.map +1 -0
  70. package/dist/components/import-export/mapping.js +38 -0
  71. package/dist/components/import-export/props.d.ts +34 -0
  72. package/dist/components/import-export/props.d.ts.map +1 -0
  73. package/dist/components/import-export/props.js +1 -0
  74. package/dist/components/import-export/report.d.ts +13 -0
  75. package/dist/components/import-export/report.d.ts.map +1 -0
  76. package/dist/components/import-export/report.js +50 -0
  77. package/dist/components/import-export/runner.d.ts +17 -0
  78. package/dist/components/import-export/runner.d.ts.map +1 -0
  79. package/dist/components/import-export/runner.js +39 -0
  80. package/dist/components/import-export/types.d.ts +100 -0
  81. package/dist/components/import-export/types.d.ts.map +1 -0
  82. package/dist/components/import-export/types.js +6 -0
  83. package/dist/components/import-export/validate.d.ts +39 -0
  84. package/dist/components/import-export/validate.d.ts.map +1 -0
  85. package/dist/components/import-export/validate.js +268 -0
  86. package/dist/components/overview/OverviewGrid.svelte +579 -0
  87. package/dist/components/overview/OverviewGrid.svelte.d.ts +5 -0
  88. package/dist/components/overview/OverviewGrid.svelte.d.ts.map +1 -0
  89. package/dist/components/overview/WidgetOptionsForm.svelte +193 -0
  90. package/dist/components/overview/WidgetOptionsForm.svelte.d.ts +5 -0
  91. package/dist/components/overview/WidgetOptionsForm.svelte.d.ts.map +1 -0
  92. package/dist/components/overview/controller.svelte.d.ts +92 -0
  93. package/dist/components/overview/controller.svelte.d.ts.map +1 -0
  94. package/dist/components/overview/controller.svelte.js +336 -0
  95. package/dist/components/overview/format.d.ts +13 -0
  96. package/dist/components/overview/format.d.ts.map +1 -0
  97. package/dist/components/overview/format.js +45 -0
  98. package/dist/components/overview/grid-types.d.ts +52 -0
  99. package/dist/components/overview/grid-types.d.ts.map +1 -0
  100. package/dist/components/overview/grid-types.js +1 -0
  101. package/dist/components/overview/index.d.ts +24 -0
  102. package/dist/components/overview/index.d.ts.map +1 -0
  103. package/dist/components/overview/index.js +22 -0
  104. package/dist/components/overview/load.d.ts +30 -0
  105. package/dist/components/overview/load.d.ts.map +1 -0
  106. package/dist/components/overview/load.js +92 -0
  107. package/dist/components/overview/markdown.d.ts +53 -0
  108. package/dist/components/overview/markdown.d.ts.map +1 -0
  109. package/dist/components/overview/markdown.js +203 -0
  110. package/dist/components/overview/model.d.ts +100 -0
  111. package/dist/components/overview/model.d.ts.map +1 -0
  112. package/dist/components/overview/model.js +460 -0
  113. package/dist/components/overview/recipe-widgets.d.ts +77 -0
  114. package/dist/components/overview/recipe-widgets.d.ts.map +1 -0
  115. package/dist/components/overview/recipe-widgets.js +151 -0
  116. package/dist/components/overview/registry.d.ts +45 -0
  117. package/dist/components/overview/registry.d.ts.map +1 -0
  118. package/dist/components/overview/registry.js +112 -0
  119. package/dist/components/overview/resize.d.ts +25 -0
  120. package/dist/components/overview/resize.d.ts.map +1 -0
  121. package/dist/components/overview/resize.js +41 -0
  122. package/dist/components/overview/schema.d.ts +34 -0
  123. package/dist/components/overview/schema.d.ts.map +1 -0
  124. package/dist/components/overview/schema.js +180 -0
  125. package/dist/components/overview/server.d.ts +14 -0
  126. package/dist/components/overview/server.d.ts.map +1 -0
  127. package/dist/components/overview/server.js +13 -0
  128. package/dist/components/overview/types.d.ts +235 -0
  129. package/dist/components/overview/types.d.ts.map +1 -0
  130. package/dist/components/overview/types.js +17 -0
  131. package/dist/components/overview/widgets/ChartWidget.svelte +111 -0
  132. package/dist/components/overview/widgets/ChartWidget.svelte.d.ts +6 -0
  133. package/dist/components/overview/widgets/ChartWidget.svelte.d.ts.map +1 -0
  134. package/dist/components/overview/widgets/MetricWidget.svelte +86 -0
  135. package/dist/components/overview/widgets/MetricWidget.svelte.d.ts +6 -0
  136. package/dist/components/overview/widgets/MetricWidget.svelte.d.ts.map +1 -0
  137. package/dist/components/overview/widgets/NoteWidget.svelte +61 -0
  138. package/dist/components/overview/widgets/NoteWidget.svelte.d.ts +5 -0
  139. package/dist/components/overview/widgets/NoteWidget.svelte.d.ts.map +1 -0
  140. package/dist/components/overview/widgets/RecordListWidget.svelte +91 -0
  141. package/dist/components/overview/widgets/RecordListWidget.svelte.d.ts +6 -0
  142. package/dist/components/overview/widgets/RecordListWidget.svelte.d.ts.map +1 -0
  143. package/dist/components/overview/widgets/ShortcutsWidget.svelte +80 -0
  144. package/dist/components/overview/widgets/ShortcutsWidget.svelte.d.ts +6 -0
  145. package/dist/components/overview/widgets/ShortcutsWidget.svelte.d.ts.map +1 -0
  146. package/dist/components/overview/widgets/core.d.ts +44 -0
  147. package/dist/components/overview/widgets/core.d.ts.map +1 -0
  148. package/dist/components/overview/widgets/core.js +222 -0
  149. package/dist/components/overview/widgets/data.d.ts +76 -0
  150. package/dist/components/overview/widgets/data.d.ts.map +1 -0
  151. package/dist/components/overview/widgets/data.js +40 -0
  152. package/dist/components/screens/DetailScreen.svelte +220 -0
  153. package/dist/components/screens/DetailScreen.svelte.d.ts +5 -0
  154. package/dist/components/screens/DetailScreen.svelte.d.ts.map +1 -0
  155. package/dist/components/screens/EditForm.svelte +313 -0
  156. package/dist/components/screens/EditForm.svelte.d.ts +5 -0
  157. package/dist/components/screens/EditForm.svelte.d.ts.map +1 -0
  158. package/dist/components/screens/ListScreen.svelte +191 -0
  159. package/dist/components/screens/ListScreen.svelte.d.ts +5 -0
  160. package/dist/components/screens/ListScreen.svelte.d.ts.map +1 -0
  161. package/dist/components/screens/RecipeScreens.svelte +237 -0
  162. package/dist/components/screens/RecipeScreens.svelte.d.ts +5 -0
  163. package/dist/components/screens/RecipeScreens.svelte.d.ts.map +1 -0
  164. package/dist/components/screens/fields.d.ts +54 -0
  165. package/dist/components/screens/fields.d.ts.map +1 -0
  166. package/dist/components/screens/fields.js +229 -0
  167. package/dist/components/screens/index.d.ts +8 -0
  168. package/dist/components/screens/index.d.ts.map +1 -0
  169. package/dist/components/screens/index.js +6 -0
  170. package/dist/components/screens/types.d.ts +215 -0
  171. package/dist/components/screens/types.d.ts.map +1 -0
  172. package/dist/components/screens/types.js +1 -0
  173. package/dist/components/screens/values.d.ts +62 -0
  174. package/dist/components/screens/values.d.ts.map +1 -0
  175. package/dist/components/screens/values.js +289 -0
  176. package/dist/components/sortable/controller.svelte.d.ts +10 -0
  177. package/dist/components/sortable/controller.svelte.d.ts.map +1 -1
  178. package/dist/components/sortable/controller.svelte.js +29 -7
  179. package/dist/components/workspace/manifest-nav.d.ts +20 -0
  180. package/dist/components/workspace/manifest-nav.d.ts.map +1 -1
  181. package/dist/components/workspace/manifest-nav.js +42 -13
  182. package/dist/i18n/server.d.ts +6 -0
  183. package/dist/i18n/server.d.ts.map +1 -1
  184. package/dist/i18n/server.js +6 -0
  185. package/dist/i18n/strings.import-export.d.ts +88 -0
  186. package/dist/i18n/strings.import-export.d.ts.map +1 -0
  187. package/dist/i18n/strings.import-export.js +88 -0
  188. package/dist/i18n/strings.overview.d.ts +100 -0
  189. package/dist/i18n/strings.overview.d.ts.map +1 -0
  190. package/dist/i18n/strings.overview.js +107 -0
  191. package/dist/i18n/strings.palette.d.ts +18 -0
  192. package/dist/i18n/strings.palette.d.ts.map +1 -0
  193. package/dist/i18n/strings.palette.js +18 -0
  194. package/dist/i18n/strings.screens.d.ts +31 -0
  195. package/dist/i18n/strings.screens.d.ts.map +1 -0
  196. package/dist/i18n/strings.screens.js +31 -0
  197. package/package.json +45 -16
@@ -0,0 +1,12 @@
1
+ import type { ColumnMapping, ImportExportField } from './types.js';
2
+ /** Normalize a header or field name so `Created At`, `created_at` and `createdAt` agree. */
3
+ export declare function normalizeHeader(value: string): string;
4
+ /**
5
+ * Suggest a target field for each source header. Matching is by normalized
6
+ * field name, then label, then declared alias; a field is claimed by at most
7
+ * one column (the first match), and non-importable fields are never targets.
8
+ */
9
+ export declare function autoMapColumns(headers: readonly string[], fields: readonly ImportExportField[]): (string | null)[];
10
+ /** Replace one column's target, clearing any other column that held the same field. */
11
+ export declare function setColumnTarget(mapping: ColumnMapping, index: number, target: string | null): (string | null)[];
12
+ //# sourceMappingURL=mapping.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mapping.d.ts","sourceRoot":"","sources":["../../../src/components/import-export/mapping.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEnE,4FAA4F;AAC5F,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMrD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,MAAM,EAAE,SAAS,iBAAiB,EAAE,GACnC,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAsBnB;AAED,uFAAuF;AACvF,wBAAgB,eAAe,CAC7B,OAAO,EAAE,aAAa,EACtB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GAAG,IAAI,GACpB,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAKnB"}
@@ -0,0 +1,38 @@
1
+ /** Normalize a header or field name so `Created At`, `created_at` and `createdAt` agree. */
2
+ export function normalizeHeader(value) {
3
+ return value
4
+ .normalize('NFKD')
5
+ .replace(/[̀-ͯ]/g, '')
6
+ .replace(/[^a-zA-Z0-9]+/g, '')
7
+ .toLowerCase();
8
+ }
9
+ /**
10
+ * Suggest a target field for each source header. Matching is by normalized
11
+ * field name, then label, then declared alias; a field is claimed by at most
12
+ * one column (the first match), and non-importable fields are never targets.
13
+ */
14
+ export function autoMapColumns(headers, fields) {
15
+ const importable = fields.filter((f) => f.importable);
16
+ const claimed = new Set();
17
+ return headers.map((header) => {
18
+ const key = normalizeHeader(header);
19
+ if (!key)
20
+ return null;
21
+ const match = importable.find((f) => !claimed.has(f.name) && normalizeHeader(f.name) === key) ??
22
+ importable.find((f) => !claimed.has(f.name) && normalizeHeader(f.label) === key) ??
23
+ importable.find((f) => !claimed.has(f.name) &&
24
+ (f.aliases ?? []).some((alias) => normalizeHeader(alias) === key));
25
+ if (!match)
26
+ return null;
27
+ claimed.add(match.name);
28
+ return match.name;
29
+ });
30
+ }
31
+ /** Replace one column's target, clearing any other column that held the same field. */
32
+ export function setColumnTarget(mapping, index, target) {
33
+ return mapping.map((current, i) => {
34
+ if (i === index)
35
+ return target;
36
+ return target !== null && current === target ? null : current;
37
+ });
38
+ }
@@ -0,0 +1,34 @@
1
+ import type { ExportFile, ImportExportField, ImportRunResult } from './types.js';
2
+ export interface ImportExportProps {
3
+ /** Columns, usually from `fieldsFromCollectionDefinition` (manifest + resolved policy). */
4
+ fields: ImportExportField[];
5
+ /** Heading context, for example the collection's plural label. */
6
+ collectionLabel?: string;
7
+ /**
8
+ * Persist one validated row. Providing it enables import; reject to fail just
9
+ * that row. `createCollectionImportExport(...).createRecord` fits.
10
+ */
11
+ createRecord?: (values: Record<string, unknown>) => Promise<unknown>;
12
+ /**
13
+ * Load the rows to export. Providing it enables export. Throw to report a
14
+ * failure. `createCollectionImportExport(...).loadRows` fits.
15
+ */
16
+ loadRows?: (options: {
17
+ signal?: AbortSignal;
18
+ }) => Promise<Array<Record<string, unknown>>>;
19
+ /** Base name for exported files. Default `export`. */
20
+ filename?: string;
21
+ /** Largest accepted file in bytes. Default 5 MiB. */
22
+ maxFileBytes?: number;
23
+ /** Most data rows accepted per file. Default 10,000. */
24
+ maxRows?: number;
25
+ /** Parallel create requests. Default 3. */
26
+ concurrency?: number;
27
+ /** Which sections to show. Default `both`; a missing callback hides its side. */
28
+ mode?: 'both' | 'import' | 'export';
29
+ /** Receives every generated file. Default triggers a browser download. */
30
+ ondownload?: (file: ExportFile) => void;
31
+ /** Called after an import run finishes (including a stopped run). */
32
+ onimported?: (result: ImportRunResult) => void;
33
+ }
34
+ //# sourceMappingURL=props.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"props.d.ts","sourceRoot":"","sources":["../../../src/components/import-export/props.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,iBAAiB,EACjB,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,iBAAiB;IAChC,2FAA2F;IAC3F,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC5B,kEAAkE;IAClE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACrE;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE;QACnB,MAAM,CAAC,EAAE,WAAW,CAAC;KACtB,KAAK,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAC9C,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,wDAAwD;IACxD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACpC,0EAA0E;IAC1E,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;IACxC,qEAAqE;IACrE,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;CAChD"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ import type { ExportFile, ImportIssue, ImportIssueCode } from './types.js';
2
+ /** English defaults; the component overrides these with translated strings. */
3
+ export declare const DEFAULT_ISSUE_MESSAGES: Record<ImportIssueCode, string>;
4
+ export type IssueMessages = Partial<Record<ImportIssueCode, string>>;
5
+ /** Human message for an issue, with `detail` appended when present. */
6
+ export declare function describeIssue(issue: ImportIssue, messages?: IssueMessages): string;
7
+ /**
8
+ * CSV error report: one row per issue with the file line, the source column,
9
+ * the target field, the offending value, and the problem. Echoed values are
10
+ * formula-neutralized because the report is meant to be opened in a spreadsheet.
11
+ */
12
+ export declare function buildErrorReport(issues: readonly ImportIssue[], messages?: IssueMessages, filename?: string): ExportFile;
13
+ //# sourceMappingURL=report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../../src/components/import-export/report.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE3E,+EAA+E;AAC/E,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,eAAe,EAAE,MAAM,CAkBlE,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC,CAAC;AAErE,uEAAuE;AACvE,wBAAgB,aAAa,CAC3B,KAAK,EAAE,WAAW,EAClB,QAAQ,GAAE,aAAkB,GAC3B,MAAM,CAGR;AAID;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,SAAS,WAAW,EAAE,EAC9B,QAAQ,GAAE,aAAkB,EAC5B,QAAQ,SAAkB,GACzB,UAAU,CAkBZ"}
@@ -0,0 +1,50 @@
1
+ import { formatDelimited, neutralizeFormula } from './csv.js';
2
+ /** English defaults; the component overrides these with translated strings. */
3
+ export const DEFAULT_ISSUE_MESSAGES = {
4
+ required: 'A value is required.',
5
+ 'invalid-integer': 'Expected a whole number.',
6
+ 'out-of-range': 'The number is outside the supported range.',
7
+ 'invalid-decimal': 'Expected a number.',
8
+ 'invalid-boolean': 'Expected true/false, yes/no or 1/0.',
9
+ 'invalid-datetime': 'Expected an ISO 8601 date or date-time (YYYY-MM-DD or YYYY-MM-DDThh:mm:ss).',
10
+ 'invalid-json': 'Expected valid JSON.',
11
+ 'invalid-enum': 'Not one of the allowed values.',
12
+ 'invalid-email': 'Expected an email address.',
13
+ 'invalid-url': 'Expected an http(s) URL.',
14
+ 'invalid-reference': 'Expected an id with no spaces.',
15
+ 'duplicate-value': 'This value repeats an earlier row.',
16
+ 'duplicate-mapping': 'More than one column maps to this field.',
17
+ 'unmapped-required': 'This required field has no column mapped.',
18
+ 'extra-cells': 'The row has more cells than the header.',
19
+ 'import-failed': 'The server rejected this row.',
20
+ };
21
+ /** Human message for an issue, with `detail` appended when present. */
22
+ export function describeIssue(issue, messages = {}) {
23
+ const base = messages[issue.code] ?? DEFAULT_ISSUE_MESSAGES[issue.code];
24
+ return issue.detail ? `${base} (${issue.detail})` : base;
25
+ }
26
+ const REPORT_HEADER = ['line', 'column', 'field', 'value', 'problem'];
27
+ /**
28
+ * CSV error report: one row per issue with the file line, the source column,
29
+ * the target field, the offending value, and the problem. Echoed values are
30
+ * formula-neutralized because the report is meant to be opened in a spreadsheet.
31
+ */
32
+ export function buildErrorReport(issues, messages = {}, filename = 'import-errors') {
33
+ const table = [
34
+ REPORT_HEADER,
35
+ ...issues.map((issue) => [
36
+ issue.line > 0 ? String(issue.line) : '',
37
+ neutralizeFormula(issue.column ?? ''),
38
+ neutralizeFormula(issue.field ?? ''),
39
+ neutralizeFormula(issue.value ?? ''),
40
+ neutralizeFormula(describeIssue(issue, messages)),
41
+ ]),
42
+ ];
43
+ return {
44
+ filename: `${filename}.csv`,
45
+ mimeType: 'text/csv;charset=utf-8',
46
+ content: `${formatDelimited(table)}`,
47
+ rowCount: issues.length,
48
+ columnCount: REPORT_HEADER.length,
49
+ };
50
+ }
@@ -0,0 +1,17 @@
1
+ import type { ImportRunResult, ValidRecord } from './types.js';
2
+ export interface RunImportOptions {
3
+ records: readonly ValidRecord[];
4
+ /** Persist one record; reject to fail just that row. */
5
+ createRecord: (values: Record<string, unknown>) => Promise<unknown>;
6
+ /** Parallel requests. Default 3; 1 keeps file order on the server. */
7
+ concurrency?: number;
8
+ signal?: AbortSignal;
9
+ onProgress?: (done: number, total: number) => void;
10
+ }
11
+ /**
12
+ * Create the validated records through the supplied collection call, one
13
+ * request per row, continuing past row failures so one bad row never loses the
14
+ * rest. Aborting stops starting new rows; in-flight rows finish and are counted.
15
+ */
16
+ export declare function runImport(options: RunImportOptions): Promise<ImportRunResult>;
17
+ //# sourceMappingURL=runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/components/import-export/runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,eAAe,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE5E,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;IAChC,wDAAwD;IACxD,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACpE,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpD;AAMD;;;;GAIG;AACH,wBAAsB,SAAS,CAC7B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,eAAe,CAAC,CAiC1B"}
@@ -0,0 +1,39 @@
1
+ function messageOf(error) {
2
+ return error instanceof Error ? error.message : String(error);
3
+ }
4
+ /**
5
+ * Create the validated records through the supplied collection call, one
6
+ * request per row, continuing past row failures so one bad row never loses the
7
+ * rest. Aborting stops starting new rows; in-flight rows finish and are counted.
8
+ */
9
+ export async function runImport(options) {
10
+ const { records, createRecord, signal, onProgress } = options;
11
+ const concurrency = Math.max(1, Math.floor(options.concurrency ?? 3));
12
+ const failed = [];
13
+ let created = 0;
14
+ let attempted = 0;
15
+ let done = 0;
16
+ let next = 0;
17
+ async function worker() {
18
+ while (next < records.length && !signal?.aborted) {
19
+ const record = records[next++];
20
+ attempted++;
21
+ try {
22
+ await createRecord(record.values);
23
+ created++;
24
+ }
25
+ catch (error) {
26
+ failed.push({
27
+ code: 'import-failed',
28
+ line: record.line,
29
+ detail: messageOf(error),
30
+ });
31
+ }
32
+ done++;
33
+ onProgress?.(done, records.length);
34
+ }
35
+ }
36
+ await Promise.all(Array.from({ length: Math.min(concurrency, records.length) }, worker));
37
+ failed.sort((a, b) => a.line - b.line);
38
+ return { attempted, created, failed, aborted: signal?.aborted === true };
39
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Shared types for the import/export module. Everything here is plain data so
3
+ * the CSV layer (csv.ts, validate.ts, export.ts, runner.ts) runs in Node with
4
+ * no Svelte, DOM, or Provider.
5
+ */
6
+ /** Column value kinds the importer understands (a superset-free mapping of the manifest types). */
7
+ export type ImportExportFieldType = 'text' | 'integer' | 'decimal' | 'boolean' | 'datetime' | 'json' | 'reference' | 'enum';
8
+ /** Presentation hint that also drives light validation (`email`, `url`). */
9
+ export type ImportExportWidget = 'textarea' | 'currency' | 'email' | 'url' | 'phone';
10
+ /**
11
+ * One importable/exportable column of a collection. Derive it from a
12
+ * collection definition and resolved field policy with
13
+ * `fieldsFromCollectionDefinition`, or author it by hand.
14
+ */
15
+ export interface ImportExportField {
16
+ /** Property name on the record (also the default CSV header). */
17
+ name: string;
18
+ /** Human label (default for header matching and `headerStyle: 'label'`). */
19
+ label: string;
20
+ type: ImportExportFieldType;
21
+ /** Required with no default: an empty or unmapped cell fails validation. */
22
+ required: boolean;
23
+ /** False for read-only, locked, hidden, or system fields: never written by import. */
24
+ importable: boolean;
25
+ /** False for hidden or tenant-internal fields: never written to an export. */
26
+ exportable: boolean;
27
+ /** Allowed values for `type: 'enum'` (matched case-insensitively). */
28
+ options?: readonly string[];
29
+ widget?: ImportExportWidget;
30
+ /** Help text surfaced beside the mapping row. */
31
+ help?: string;
32
+ /** A resolved policy/manifest default; fills empty or unmapped cells. */
33
+ hasDefault?: boolean;
34
+ defaultValue?: unknown;
35
+ /**
36
+ * An org-locked field: not importable, but its resolved default is still
37
+ * written to every record. A non-importable field that is not `locked`
38
+ * (hidden, tenant, system) never enters the payload, defaults included.
39
+ */
40
+ locked?: boolean;
41
+ /** Reject a second row repeating the same value within one file. */
42
+ unique?: boolean;
43
+ /** Extra header spellings the auto-mapper should accept. */
44
+ aliases?: readonly string[];
45
+ }
46
+ /** A parsed delimited file: header row plus data rows with their file line numbers. */
47
+ export interface ParsedTable {
48
+ delimiter: string;
49
+ headers: string[];
50
+ rows: string[][];
51
+ /** 1-based file line each entry of `rows` starts on (for error reports). */
52
+ lines: number[];
53
+ }
54
+ /** Per-source-column target: a field name, or null to skip the column. */
55
+ export type ColumnMapping = ReadonlyArray<string | null>;
56
+ export type ImportIssueCode = 'required' | 'invalid-integer' | 'out-of-range' | 'invalid-decimal' | 'invalid-boolean' | 'invalid-datetime' | 'invalid-json' | 'invalid-enum' | 'invalid-email' | 'invalid-url' | 'invalid-reference' | 'duplicate-value' | 'duplicate-mapping' | 'unmapped-required' | 'extra-cells' | 'import-failed';
57
+ /** One validation or import problem. `line` is 0 for mapping-level issues. */
58
+ export interface ImportIssue {
59
+ code: ImportIssueCode;
60
+ /** 1-based file line of the offending record; 0 when not row-specific. */
61
+ line: number;
62
+ field?: string;
63
+ /** Source column header the value came from. */
64
+ column?: string;
65
+ /** The offending raw cell (truncated to 200 chars). */
66
+ value?: string;
67
+ /** Extra context (the server error for `import-failed`, the allowed values for `invalid-enum`). */
68
+ detail?: string;
69
+ }
70
+ /** A row that passed validation, ready for `createRecord`. */
71
+ export interface ValidRecord {
72
+ /** Index into the parsed `rows`. */
73
+ index: number;
74
+ line: number;
75
+ values: Record<string, unknown>;
76
+ }
77
+ export interface ValidationResult {
78
+ totalRows: number;
79
+ records: ValidRecord[];
80
+ /** Row-level issues, capped at `maxIssues`; see `issuesTruncated`. */
81
+ issues: ImportIssue[];
82
+ issuesTruncated: boolean;
83
+ /** Mapping-level issues (duplicate target, unmapped required field). */
84
+ mappingIssues: ImportIssue[];
85
+ invalidRows: number;
86
+ }
87
+ export interface ImportRunResult {
88
+ attempted: number;
89
+ created: number;
90
+ failed: ImportIssue[];
91
+ aborted: boolean;
92
+ }
93
+ export interface ExportFile {
94
+ filename: string;
95
+ mimeType: string;
96
+ content: string;
97
+ rowCount: number;
98
+ columnCount: number;
99
+ }
100
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/import-export/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,mGAAmG;AACnG,MAAM,MAAM,qBAAqB,GAC7B,MAAM,GACN,SAAS,GACT,SAAS,GACT,SAAS,GACT,UAAU,GACV,MAAM,GACN,WAAW,GACX,MAAM,CAAC;AAEX,4EAA4E;AAC5E,MAAM,MAAM,kBAAkB,GAC1B,UAAU,GACV,UAAU,GACV,OAAO,GACP,KAAK,GACL,OAAO,CAAC;AAEZ;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,qBAAqB,CAAC;IAC5B,4EAA4E;IAC5E,QAAQ,EAAE,OAAO,CAAC;IAClB,sFAAsF;IACtF,UAAU,EAAE,OAAO,CAAC;IACpB,8EAA8E;IAC9E,UAAU,EAAE,OAAO,CAAC;IACpB,sEAAsE;IACtE,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,iDAAiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,oEAAoE;IACpE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,4DAA4D;IAC5D,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED,uFAAuF;AACvF,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC;IACjB,4EAA4E;IAC5E,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,0EAA0E;AAC1E,MAAM,MAAM,aAAa,GAAG,aAAa,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AAEzD,MAAM,MAAM,eAAe,GACvB,UAAU,GACV,iBAAiB,GACjB,cAAc,GACd,iBAAiB,GACjB,iBAAiB,GACjB,kBAAkB,GAClB,cAAc,GACd,cAAc,GACd,eAAe,GACf,aAAa,GACb,mBAAmB,GACnB,iBAAiB,GACjB,mBAAmB,GACnB,mBAAmB,GACnB,aAAa,GACb,eAAe,CAAC;AAEpB,8EAA8E;AAC9E,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,eAAe,CAAC;IACtB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mGAAmG;IACnG,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,8DAA8D;AAC9D,MAAM,WAAW,WAAW;IAC1B,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,sEAAsE;IACtE,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,eAAe,EAAE,OAAO,CAAC;IACzB,wEAAwE;IACxE,aAAa,EAAE,WAAW,EAAE,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Shared types for the import/export module. Everything here is plain data so
3
+ * the CSV layer (csv.ts, validate.ts, export.ts, runner.ts) runs in Node with
4
+ * no Svelte, DOM, or Provider.
5
+ */
6
+ export {};
@@ -0,0 +1,39 @@
1
+ import type { ColumnMapping, ImportExportField, ImportIssue, ImportIssueCode, ValidationResult } from './types.js';
2
+ export type CoerceResult = {
3
+ ok: true;
4
+ value: unknown;
5
+ } | {
6
+ ok: false;
7
+ code: ImportIssueCode;
8
+ detail?: string;
9
+ };
10
+ /**
11
+ * Coerce one non-empty, trimmed cell to the field's type. Empty-cell handling
12
+ * (required, defaults) belongs to the caller.
13
+ *
14
+ * Datetimes are strict ISO 8601 (`2026-03-05`, `2026-03-05T09:30`,
15
+ * `2026-03-05T09:30:00-05:00`); a missing offset means UTC. Locale formats
16
+ * such as `3/5/2026` are rejected rather than guessed.
17
+ */
18
+ export declare function coerceCell(raw: string, field: ImportExportField): CoerceResult;
19
+ /** Mapping-level problems: two columns on one field, or a required field with no source. */
20
+ export declare function validateMapping(headers: readonly string[], mapping: ColumnMapping, fields: readonly ImportExportField[]): ImportIssue[];
21
+ export interface ValidateRowsInput {
22
+ headers: readonly string[];
23
+ /** Data rows (header excluded). */
24
+ rows: ReadonlyArray<ReadonlyArray<string>>;
25
+ /** 1-based file line of each row. Defaults to row index + 2. */
26
+ lines?: readonly number[];
27
+ mapping: ColumnMapping;
28
+ fields: readonly ImportExportField[];
29
+ /** Stop collecting row issues past this many (counts keep running). */
30
+ maxIssues?: number;
31
+ }
32
+ /**
33
+ * Validate and coerce every row against the mapped fields. Pure: this is the
34
+ * dry run, nothing is written. A row with any issue is excluded from
35
+ * `records`; `invalidRows` counts it once however many issues it has. While
36
+ * `mappingIssues` is non-empty `records` is empty.
37
+ */
38
+ export declare function validateRows(input: ValidateRowsInput): ValidationResult;
39
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../src/components/import-export/validate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,aAAa,EACb,iBAAiB,EACjB,WAAW,EACX,eAAe,EACf,gBAAgB,EAEjB,MAAM,YAAY,CAAC;AAapB,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC5B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,eAAe,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AA8B1D;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,iBAAiB,GACvB,YAAY,CA8Dd;AAED,4FAA4F;AAC5F,wBAAgB,eAAe,CAC7B,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,SAAS,iBAAiB,EAAE,GACnC,WAAW,EAAE,CA0Bf;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,mCAAmC;IACnC,IAAI,EAAE,aAAa,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC;IAC3C,gEAAgE;IAChE,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,OAAO,EAAE,aAAa,CAAC;IACvB,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACrC,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAQD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,gBAAgB,CAyHvE"}
@@ -0,0 +1,268 @@
1
+ import { restoreNeutralized } from './csv.js';
2
+ const MAX_ECHOED_VALUE = 200;
3
+ const TRUE_WORDS = new Set(['true', 't', 'yes', 'y', '1', 'on']);
4
+ const FALSE_WORDS = new Set(['false', 'f', 'no', 'n', '0', 'off']);
5
+ const INTEGER = /^[+-]?\d+$/;
6
+ const DECIMAL = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/;
7
+ const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
8
+ const DATETIME = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,9}))?)?(Z|[+-]\d{2}:?\d{2})?)?$/;
9
+ function parseDatetime(raw) {
10
+ const m = DATETIME.exec(raw);
11
+ if (!m)
12
+ return null;
13
+ const [, y, mo, d, h = '00', mi = '00', s = '00', frac = '0', zone] = m;
14
+ const year = Number(y);
15
+ const month = Number(mo);
16
+ const day = Number(d);
17
+ const probe = new Date(Date.UTC(year, month - 1, day));
18
+ if (probe.getUTCFullYear() !== year ||
19
+ probe.getUTCMonth() !== month - 1 ||
20
+ probe.getUTCDate() !== day ||
21
+ Number(h) > 23 ||
22
+ Number(mi) > 59 ||
23
+ Number(s) > 59) {
24
+ return null;
25
+ }
26
+ const millis = frac.slice(0, 3).padEnd(3, '0');
27
+ let offset = 'Z';
28
+ if (zone && zone !== 'Z') {
29
+ offset = zone.includes(':') ? zone : `${zone.slice(0, 3)}:${zone.slice(3)}`;
30
+ }
31
+ const iso = `${y}-${mo}-${d}T${h}:${mi}:${s}.${millis}${offset}`;
32
+ const time = Date.parse(iso);
33
+ return Number.isNaN(time) ? null : new Date(time).toISOString();
34
+ }
35
+ /**
36
+ * Coerce one non-empty, trimmed cell to the field's type. Empty-cell handling
37
+ * (required, defaults) belongs to the caller.
38
+ *
39
+ * Datetimes are strict ISO 8601 (`2026-03-05`, `2026-03-05T09:30`,
40
+ * `2026-03-05T09:30:00-05:00`); a missing offset means UTC. Locale formats
41
+ * such as `3/5/2026` are rejected rather than guessed.
42
+ */
43
+ export function coerceCell(raw, field) {
44
+ switch (field.type) {
45
+ case 'integer': {
46
+ if (!INTEGER.test(raw))
47
+ return { ok: false, code: 'invalid-integer' };
48
+ const n = Number(raw);
49
+ if (!Number.isSafeInteger(n))
50
+ return { ok: false, code: 'out-of-range' };
51
+ return { ok: true, value: n };
52
+ }
53
+ case 'decimal': {
54
+ if (!DECIMAL.test(raw))
55
+ return { ok: false, code: 'invalid-decimal' };
56
+ const n = Number(raw);
57
+ if (!Number.isFinite(n))
58
+ return { ok: false, code: 'out-of-range' };
59
+ return { ok: true, value: n };
60
+ }
61
+ case 'boolean': {
62
+ const word = raw.toLowerCase();
63
+ if (TRUE_WORDS.has(word))
64
+ return { ok: true, value: true };
65
+ if (FALSE_WORDS.has(word))
66
+ return { ok: true, value: false };
67
+ return { ok: false, code: 'invalid-boolean' };
68
+ }
69
+ case 'datetime': {
70
+ const iso = parseDatetime(raw);
71
+ return iso === null
72
+ ? { ok: false, code: 'invalid-datetime' }
73
+ : { ok: true, value: iso };
74
+ }
75
+ case 'json': {
76
+ try {
77
+ return { ok: true, value: JSON.parse(raw) };
78
+ }
79
+ catch {
80
+ return { ok: false, code: 'invalid-json' };
81
+ }
82
+ }
83
+ case 'enum': {
84
+ const options = field.options ?? [];
85
+ const hit = options.find((o) => o.toLowerCase() === raw.toLowerCase());
86
+ return hit === undefined
87
+ ? { ok: false, code: 'invalid-enum', detail: options.join(', ') }
88
+ : { ok: true, value: hit };
89
+ }
90
+ case 'reference':
91
+ return /\s/.test(raw)
92
+ ? { ok: false, code: 'invalid-reference' }
93
+ : { ok: true, value: raw };
94
+ default: {
95
+ const text = restoreNeutralized(raw);
96
+ if (field.widget === 'email' && !EMAIL.test(text)) {
97
+ return { ok: false, code: 'invalid-email' };
98
+ }
99
+ if (field.widget === 'url') {
100
+ try {
101
+ const url = new URL(text);
102
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
103
+ return { ok: false, code: 'invalid-url' };
104
+ }
105
+ }
106
+ catch {
107
+ return { ok: false, code: 'invalid-url' };
108
+ }
109
+ }
110
+ return { ok: true, value: text };
111
+ }
112
+ }
113
+ }
114
+ /** Mapping-level problems: two columns on one field, or a required field with no source. */
115
+ export function validateMapping(headers, mapping, fields) {
116
+ const issues = [];
117
+ const seen = new Map();
118
+ mapping.forEach((target, index) => {
119
+ if (target === null)
120
+ return;
121
+ if (seen.has(target)) {
122
+ issues.push({
123
+ code: 'duplicate-mapping',
124
+ line: 0,
125
+ field: target,
126
+ column: headers[index],
127
+ });
128
+ }
129
+ seen.set(target, index);
130
+ });
131
+ for (const field of fields) {
132
+ if (field.required &&
133
+ field.importable &&
134
+ !field.hasDefault &&
135
+ !seen.has(field.name)) {
136
+ issues.push({ code: 'unmapped-required', line: 0, field: field.name });
137
+ }
138
+ }
139
+ return issues;
140
+ }
141
+ function echo(value) {
142
+ return value.length > MAX_ECHOED_VALUE
143
+ ? `${value.slice(0, MAX_ECHOED_VALUE)}…`
144
+ : value;
145
+ }
146
+ /**
147
+ * Validate and coerce every row against the mapped fields. Pure: this is the
148
+ * dry run, nothing is written. A row with any issue is excluded from
149
+ * `records`; `invalidRows` counts it once however many issues it has. While
150
+ * `mappingIssues` is non-empty `records` is empty.
151
+ */
152
+ export function validateRows(input) {
153
+ const { headers, rows, mapping, fields } = input;
154
+ const maxIssues = input.maxIssues ?? 1000;
155
+ const byName = new Map(fields.map((f) => [f.name, f]));
156
+ const mappingIssues = validateMapping(headers, mapping, fields);
157
+ const blocked = new Set(mappingIssues
158
+ .filter((i) => i.code === 'duplicate-mapping')
159
+ .map((i) => i.field));
160
+ const columns = [];
161
+ mapping.forEach((target, index) => {
162
+ const field = target === null ? undefined : byName.get(target);
163
+ if (field?.importable && !blocked.has(field.name)) {
164
+ columns.push({ index, field });
165
+ }
166
+ });
167
+ const mappedNames = new Set(columns.map((c) => c.field.name));
168
+ const records = [];
169
+ const issues = [];
170
+ let issuesTruncated = false;
171
+ let invalidRows = 0;
172
+ const unique = new Map();
173
+ for (const field of fields)
174
+ if (field.unique)
175
+ unique.set(field.name, new Set());
176
+ rows.forEach((row, index) => {
177
+ const line = input.lines?.[index] ?? index + 2;
178
+ const rowIssues = [];
179
+ const values = {};
180
+ const pendingUnique = [];
181
+ if (row.length > headers.length) {
182
+ rowIssues.push({
183
+ code: 'extra-cells',
184
+ line,
185
+ detail: `${row.length} cells, ${headers.length} columns`,
186
+ });
187
+ }
188
+ for (const { index: col, field } of columns) {
189
+ const raw = (row[col] ?? '').trim();
190
+ if (raw === '')
191
+ continue;
192
+ const result = coerceCell(raw, field);
193
+ if (!result.ok) {
194
+ rowIssues.push({
195
+ code: result.code,
196
+ line,
197
+ field: field.name,
198
+ column: headers[col],
199
+ value: echo(raw),
200
+ detail: result.detail,
201
+ });
202
+ continue;
203
+ }
204
+ if (field.unique) {
205
+ const key = JSON.stringify(result.value);
206
+ const set = unique.get(field.name);
207
+ if (set?.has(key)) {
208
+ rowIssues.push({
209
+ code: 'duplicate-value',
210
+ line,
211
+ field: field.name,
212
+ column: headers[col],
213
+ value: echo(raw),
214
+ });
215
+ continue;
216
+ }
217
+ if (set)
218
+ pendingUnique.push([set, key]);
219
+ }
220
+ values[field.name] = result.value;
221
+ }
222
+ for (const field of fields) {
223
+ // Only importable and org-locked fields take a default; hidden, tenant
224
+ // and system fields never enter the payload.
225
+ if (!(field.name in values) &&
226
+ field.hasDefault &&
227
+ (field.importable || field.locked === true)) {
228
+ values[field.name] = field.defaultValue;
229
+ }
230
+ if (field.required && field.importable && !(field.name in values)) {
231
+ const mappedColumn = columns.find((c) => c.field.name === field.name);
232
+ // An unmapped required field is a mapping issue, reported once.
233
+ if (mappedNames.has(field.name)) {
234
+ rowIssues.push({
235
+ code: 'required',
236
+ line,
237
+ field: field.name,
238
+ column: mappedColumn ? headers[mappedColumn.index] : undefined,
239
+ });
240
+ }
241
+ }
242
+ }
243
+ if (rowIssues.length > 0) {
244
+ invalidRows++;
245
+ for (const issue of rowIssues) {
246
+ if (issues.length < maxIssues)
247
+ issues.push(issue);
248
+ else
249
+ issuesTruncated = true;
250
+ }
251
+ }
252
+ else {
253
+ for (const [set, key] of pendingUnique)
254
+ set.add(key);
255
+ records.push({ index, line, values });
256
+ }
257
+ });
258
+ return {
259
+ totalRows: rows.length,
260
+ // Nothing is importable until the mapping itself is sound: an unmapped
261
+ // required field would otherwise be written without its value.
262
+ records: mappingIssues.length > 0 ? [] : records,
263
+ issues,
264
+ issuesTruncated,
265
+ mappingIssues,
266
+ invalidRows,
267
+ };
268
+ }