@happyvertical/smrt-svelte 0.55.7 → 0.55.8
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/AGENTS.md +7 -44
- package/README.md +112 -1
- package/agents/browser-ai.md +47 -0
- package/agents/command-palette.md +85 -0
- package/agents/import-export.md +62 -0
- package/agents/overview-surfaces.md +255 -0
- package/agents/screens.md +57 -0
- package/dist/browser-ai/adapters/tts/browser-synthesis.d.ts +5 -0
- package/dist/browser-ai/adapters/tts/browser-synthesis.d.ts.map +1 -1
- package/dist/browser-ai/adapters/tts/browser-synthesis.js +83 -55
- package/dist/components/command-palette/CommandPalette.svelte +75 -0
- package/dist/components/command-palette/CommandPalette.svelte.d.ts +22 -0
- package/dist/components/command-palette/CommandPalette.svelte.d.ts.map +1 -0
- package/dist/components/command-palette/CommandPaletteDialog.svelte +406 -0
- package/dist/components/command-palette/CommandPaletteDialog.svelte.d.ts +18 -0
- package/dist/components/command-palette/CommandPaletteDialog.svelte.d.ts.map +1 -0
- package/dist/components/command-palette/CommandPaletteTrigger.svelte +91 -0
- package/dist/components/command-palette/CommandPaletteTrigger.svelte.d.ts +17 -0
- package/dist/components/command-palette/CommandPaletteTrigger.svelte.d.ts.map +1 -0
- package/dist/components/command-palette/context.d.ts +18 -0
- package/dist/components/command-palette/context.d.ts.map +1 -0
- package/dist/components/command-palette/context.js +38 -0
- package/dist/components/command-palette/controller.svelte.d.ts +70 -0
- package/dist/components/command-palette/controller.svelte.d.ts.map +1 -0
- package/dist/components/command-palette/controller.svelte.js +379 -0
- package/dist/components/command-palette/hotkeys.d.ts +30 -0
- package/dist/components/command-palette/hotkeys.d.ts.map +1 -0
- package/dist/components/command-palette/hotkeys.js +161 -0
- package/dist/components/command-palette/index.d.ts +19 -0
- package/dist/components/command-palette/index.d.ts.map +1 -0
- package/dist/components/command-palette/index.js +17 -0
- package/dist/components/command-palette/match.d.ts +38 -0
- package/dist/components/command-palette/match.d.ts.map +1 -0
- package/dist/components/command-palette/match.js +172 -0
- package/dist/components/command-palette/providers.d.ts +159 -0
- package/dist/components/command-palette/providers.d.ts.map +1 -0
- package/dist/components/command-palette/providers.js +345 -0
- package/dist/components/command-palette/rank.d.ts +20 -0
- package/dist/components/command-palette/rank.d.ts.map +1 -0
- package/dist/components/command-palette/rank.js +74 -0
- package/dist/components/command-palette/types.d.ts +119 -0
- package/dist/components/command-palette/types.d.ts.map +1 -0
- package/dist/components/command-palette/types.js +10 -0
- package/dist/components/command-palette/use-provider.svelte.d.ts +14 -0
- package/dist/components/command-palette/use-provider.svelte.d.ts.map +1 -0
- package/dist/components/command-palette/use-provider.svelte.js +23 -0
- package/dist/components/import-export/ImportExport.svelte +678 -0
- package/dist/components/import-export/ImportExport.svelte.d.ts +5 -0
- package/dist/components/import-export/ImportExport.svelte.d.ts.map +1 -0
- package/dist/components/import-export/collection-adapter.d.ts +35 -0
- package/dist/components/import-export/collection-adapter.d.ts.map +1 -0
- package/dist/components/import-export/collection-adapter.js +42 -0
- package/dist/components/import-export/csv.d.ts +60 -0
- package/dist/components/import-export/csv.d.ts.map +1 -0
- package/dist/components/import-export/csv.js +192 -0
- package/dist/components/import-export/download.d.ts +4 -0
- package/dist/components/import-export/download.d.ts.map +1 -0
- package/dist/components/import-export/download.js +17 -0
- package/dist/components/import-export/export.d.ts +25 -0
- package/dist/components/import-export/export.d.ts.map +1 -0
- package/dist/components/import-export/export.js +68 -0
- package/dist/components/import-export/fields.d.ts +48 -0
- package/dist/components/import-export/fields.d.ts.map +1 -0
- package/dist/components/import-export/fields.js +103 -0
- package/dist/components/import-export/index.d.ts +12 -0
- package/dist/components/import-export/index.d.ts.map +1 -0
- package/dist/components/import-export/index.js +9 -0
- package/dist/components/import-export/mapping.d.ts +12 -0
- package/dist/components/import-export/mapping.d.ts.map +1 -0
- package/dist/components/import-export/mapping.js +38 -0
- package/dist/components/import-export/props.d.ts +34 -0
- package/dist/components/import-export/props.d.ts.map +1 -0
- package/dist/components/import-export/props.js +1 -0
- package/dist/components/import-export/report.d.ts +13 -0
- package/dist/components/import-export/report.d.ts.map +1 -0
- package/dist/components/import-export/report.js +50 -0
- package/dist/components/import-export/runner.d.ts +17 -0
- package/dist/components/import-export/runner.d.ts.map +1 -0
- package/dist/components/import-export/runner.js +39 -0
- package/dist/components/import-export/types.d.ts +100 -0
- package/dist/components/import-export/types.d.ts.map +1 -0
- package/dist/components/import-export/types.js +6 -0
- package/dist/components/import-export/validate.d.ts +39 -0
- package/dist/components/import-export/validate.d.ts.map +1 -0
- package/dist/components/import-export/validate.js +268 -0
- package/dist/components/overview/OverviewGrid.svelte +579 -0
- package/dist/components/overview/OverviewGrid.svelte.d.ts +5 -0
- package/dist/components/overview/OverviewGrid.svelte.d.ts.map +1 -0
- package/dist/components/overview/WidgetOptionsForm.svelte +193 -0
- package/dist/components/overview/WidgetOptionsForm.svelte.d.ts +5 -0
- package/dist/components/overview/WidgetOptionsForm.svelte.d.ts.map +1 -0
- package/dist/components/overview/controller.svelte.d.ts +92 -0
- package/dist/components/overview/controller.svelte.d.ts.map +1 -0
- package/dist/components/overview/controller.svelte.js +336 -0
- package/dist/components/overview/format.d.ts +13 -0
- package/dist/components/overview/format.d.ts.map +1 -0
- package/dist/components/overview/format.js +45 -0
- package/dist/components/overview/grid-types.d.ts +52 -0
- package/dist/components/overview/grid-types.d.ts.map +1 -0
- package/dist/components/overview/grid-types.js +1 -0
- package/dist/components/overview/index.d.ts +24 -0
- package/dist/components/overview/index.d.ts.map +1 -0
- package/dist/components/overview/index.js +22 -0
- package/dist/components/overview/load.d.ts +30 -0
- package/dist/components/overview/load.d.ts.map +1 -0
- package/dist/components/overview/load.js +92 -0
- package/dist/components/overview/markdown.d.ts +53 -0
- package/dist/components/overview/markdown.d.ts.map +1 -0
- package/dist/components/overview/markdown.js +203 -0
- package/dist/components/overview/model.d.ts +100 -0
- package/dist/components/overview/model.d.ts.map +1 -0
- package/dist/components/overview/model.js +460 -0
- package/dist/components/overview/recipe-widgets.d.ts +77 -0
- package/dist/components/overview/recipe-widgets.d.ts.map +1 -0
- package/dist/components/overview/recipe-widgets.js +151 -0
- package/dist/components/overview/registry.d.ts +45 -0
- package/dist/components/overview/registry.d.ts.map +1 -0
- package/dist/components/overview/registry.js +112 -0
- package/dist/components/overview/resize.d.ts +25 -0
- package/dist/components/overview/resize.d.ts.map +1 -0
- package/dist/components/overview/resize.js +41 -0
- package/dist/components/overview/schema.d.ts +34 -0
- package/dist/components/overview/schema.d.ts.map +1 -0
- package/dist/components/overview/schema.js +180 -0
- package/dist/components/overview/server.d.ts +14 -0
- package/dist/components/overview/server.d.ts.map +1 -0
- package/dist/components/overview/server.js +13 -0
- package/dist/components/overview/types.d.ts +235 -0
- package/dist/components/overview/types.d.ts.map +1 -0
- package/dist/components/overview/types.js +17 -0
- package/dist/components/overview/widgets/ChartWidget.svelte +111 -0
- package/dist/components/overview/widgets/ChartWidget.svelte.d.ts +6 -0
- package/dist/components/overview/widgets/ChartWidget.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/MetricWidget.svelte +86 -0
- package/dist/components/overview/widgets/MetricWidget.svelte.d.ts +6 -0
- package/dist/components/overview/widgets/MetricWidget.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/NoteWidget.svelte +61 -0
- package/dist/components/overview/widgets/NoteWidget.svelte.d.ts +5 -0
- package/dist/components/overview/widgets/NoteWidget.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/RecordListWidget.svelte +91 -0
- package/dist/components/overview/widgets/RecordListWidget.svelte.d.ts +6 -0
- package/dist/components/overview/widgets/RecordListWidget.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/ShortcutsWidget.svelte +80 -0
- package/dist/components/overview/widgets/ShortcutsWidget.svelte.d.ts +6 -0
- package/dist/components/overview/widgets/ShortcutsWidget.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/core.d.ts +44 -0
- package/dist/components/overview/widgets/core.d.ts.map +1 -0
- package/dist/components/overview/widgets/core.js +222 -0
- package/dist/components/overview/widgets/data.d.ts +76 -0
- package/dist/components/overview/widgets/data.d.ts.map +1 -0
- package/dist/components/overview/widgets/data.js +40 -0
- package/dist/components/screens/DetailScreen.svelte +220 -0
- package/dist/components/screens/DetailScreen.svelte.d.ts +5 -0
- package/dist/components/screens/DetailScreen.svelte.d.ts.map +1 -0
- package/dist/components/screens/EditForm.svelte +313 -0
- package/dist/components/screens/EditForm.svelte.d.ts +5 -0
- package/dist/components/screens/EditForm.svelte.d.ts.map +1 -0
- package/dist/components/screens/ListScreen.svelte +191 -0
- package/dist/components/screens/ListScreen.svelte.d.ts +5 -0
- package/dist/components/screens/ListScreen.svelte.d.ts.map +1 -0
- package/dist/components/screens/RecipeScreens.svelte +237 -0
- package/dist/components/screens/RecipeScreens.svelte.d.ts +5 -0
- package/dist/components/screens/RecipeScreens.svelte.d.ts.map +1 -0
- package/dist/components/screens/fields.d.ts +54 -0
- package/dist/components/screens/fields.d.ts.map +1 -0
- package/dist/components/screens/fields.js +229 -0
- package/dist/components/screens/index.d.ts +8 -0
- package/dist/components/screens/index.d.ts.map +1 -0
- package/dist/components/screens/index.js +6 -0
- package/dist/components/screens/types.d.ts +215 -0
- package/dist/components/screens/types.d.ts.map +1 -0
- package/dist/components/screens/types.js +1 -0
- package/dist/components/screens/values.d.ts +62 -0
- package/dist/components/screens/values.d.ts.map +1 -0
- package/dist/components/screens/values.js +289 -0
- package/dist/components/sortable/controller.svelte.d.ts +10 -0
- package/dist/components/sortable/controller.svelte.d.ts.map +1 -1
- package/dist/components/sortable/controller.svelte.js +29 -7
- package/dist/components/workspace/manifest-nav.d.ts +20 -0
- package/dist/components/workspace/manifest-nav.d.ts.map +1 -1
- package/dist/components/workspace/manifest-nav.js +42 -13
- package/dist/i18n/server.d.ts +6 -0
- package/dist/i18n/server.d.ts.map +1 -1
- package/dist/i18n/server.js +6 -0
- package/dist/i18n/strings.import-export.d.ts +88 -0
- package/dist/i18n/strings.import-export.d.ts.map +1 -0
- package/dist/i18n/strings.import-export.js +88 -0
- package/dist/i18n/strings.overview.d.ts +100 -0
- package/dist/i18n/strings.overview.d.ts.map +1 -0
- package/dist/i18n/strings.overview.js +107 -0
- package/dist/i18n/strings.palette.d.ts +18 -0
- package/dist/i18n/strings.palette.d.ts.map +1 -0
- package/dist/i18n/strings.palette.js +18 -0
- package/dist/i18n/strings.screens.d.ts +31 -0
- package/dist/i18n/strings.screens.d.ts.map +1 -0
- package/dist/i18n/strings.screens.js +31 -0
- package/package.json +45 -16
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type SmrtCrudFetchers, type SmrtWebCollectionDefinition } from '@happyvertical/smrt-web';
|
|
2
|
+
import { type FieldsFromDefinitionOptions } from './fields.js';
|
|
3
|
+
import type { ImportExportField } from './types.js';
|
|
4
|
+
export interface LoadRowsOptions {
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
/** Rows per request. Default 500. */
|
|
7
|
+
pageSize?: number;
|
|
8
|
+
/** Safety cap on exported rows. Default 50000. */
|
|
9
|
+
maxRows?: number;
|
|
10
|
+
/** Extra list params (for example `where` or `orderBy`) merged into each page request. */
|
|
11
|
+
query?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
export interface CollectionImportExport {
|
|
14
|
+
fields: ImportExportField[];
|
|
15
|
+
createRecord: (values: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
16
|
+
loadRows: (options?: LoadRowsOptions) => Promise<Array<Record<string, unknown>>>;
|
|
17
|
+
}
|
|
18
|
+
export declare class ExportLimitError extends Error {
|
|
19
|
+
readonly maxRows: number;
|
|
20
|
+
constructor(maxRows: number);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Bind the import/export UI to a collection's generated CRUD fetchers. Rows go
|
|
24
|
+
* through the same REST create/list routes (and so the same authorization,
|
|
25
|
+
* tenancy, and validation) as any other client; this layer never writes SQL.
|
|
26
|
+
*
|
|
27
|
+
* `loadRows` pages with `limit`/`offset` and fails with
|
|
28
|
+
* {@link ExportLimitError} rather than silently truncating at `maxRows`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createCollectionImportExport(input: {
|
|
31
|
+
definition: Pick<SmrtWebCollectionDefinition, 'name' | 'fields'>;
|
|
32
|
+
fetchers: Pick<SmrtCrudFetchers, 'list' | 'create'>;
|
|
33
|
+
fieldOptions?: FieldsFromDefinitionOptions;
|
|
34
|
+
}): CollectionImportExport;
|
|
35
|
+
//# sourceMappingURL=collection-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collection-adapter.d.ts","sourceRoot":"","sources":["../../../src/components/import-export/collection-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,2BAA2B,EAGjC,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,KAAK,2BAA2B,EAEjC,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0FAA0F;IAC1F,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC5B,YAAY,EAAE,CACZ,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC5B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACtC,QAAQ,EAAE,CACR,OAAO,CAAC,EAAE,eAAe,KACtB,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;CAC9C;AAED,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBACb,OAAO,EAAE,MAAM;CAK5B;AAED;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE;IAClD,UAAU,EAAE,IAAI,CAAC,2BAA2B,EAAE,MAAM,GAAG,QAAQ,CAAC,CAAC;IACjE,QAAQ,EAAE,IAAI,CAAC,gBAAgB,EAAE,MAAM,GAAG,QAAQ,CAAC,CAAC;IACpD,YAAY,CAAC,EAAE,2BAA2B,CAAC;CAC5C,GAAG,sBAAsB,CAwBzB"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { unwrapItemResult, unwrapListResult, } from '@happyvertical/smrt-web';
|
|
2
|
+
import { fieldsFromCollectionDefinition, } from './fields.js';
|
|
3
|
+
export class ExportLimitError extends Error {
|
|
4
|
+
maxRows;
|
|
5
|
+
constructor(maxRows) {
|
|
6
|
+
super(`Export stopped at the ${maxRows} row limit`);
|
|
7
|
+
this.name = 'ExportLimitError';
|
|
8
|
+
this.maxRows = maxRows;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Bind the import/export UI to a collection's generated CRUD fetchers. Rows go
|
|
13
|
+
* through the same REST create/list routes (and so the same authorization,
|
|
14
|
+
* tenancy, and validation) as any other client; this layer never writes SQL.
|
|
15
|
+
*
|
|
16
|
+
* `loadRows` pages with `limit`/`offset` and fails with
|
|
17
|
+
* {@link ExportLimitError} rather than silently truncating at `maxRows`.
|
|
18
|
+
*/
|
|
19
|
+
export function createCollectionImportExport(input) {
|
|
20
|
+
const { definition, fetchers } = input;
|
|
21
|
+
return {
|
|
22
|
+
fields: fieldsFromCollectionDefinition(definition, input.fieldOptions),
|
|
23
|
+
async createRecord(values) {
|
|
24
|
+
const result = await fetchers.create(values);
|
|
25
|
+
return unwrapItemResult(result, `create(${definition.name})`);
|
|
26
|
+
},
|
|
27
|
+
async loadRows(options = {}) {
|
|
28
|
+
const pageSize = Math.max(1, options.pageSize ?? 500);
|
|
29
|
+
const maxRows = options.maxRows ?? 50_000;
|
|
30
|
+
const rows = [];
|
|
31
|
+
for (let offset = 0;; offset += pageSize) {
|
|
32
|
+
options.signal?.throwIfAborted();
|
|
33
|
+
const page = unwrapListResult(await fetchers.list({ ...options.query, limit: pageSize, offset }), definition.name);
|
|
34
|
+
rows.push(...page);
|
|
35
|
+
if (rows.length > maxRows)
|
|
36
|
+
throw new ExportLimitError(maxRows);
|
|
37
|
+
if (page.length < pageSize)
|
|
38
|
+
return rows;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 4180 delimited-text parsing and formatting. Pure; no DOM.
|
|
3
|
+
*/
|
|
4
|
+
export declare class CsvParseError extends Error {
|
|
5
|
+
readonly code: 'unterminated-quote' | 'too-many-rows' | 'empty';
|
|
6
|
+
readonly line: number;
|
|
7
|
+
constructor(code: 'unterminated-quote' | 'too-many-rows' | 'empty', line: number, message: string);
|
|
8
|
+
}
|
|
9
|
+
/** Strip a UTF-8 byte-order mark left by spreadsheet exports. */
|
|
10
|
+
export declare function stripBom(text: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Pick the delimiter that occurs most often, outside quotes, on the first
|
|
13
|
+
* non-empty line. Ties and empty input resolve to a comma.
|
|
14
|
+
*/
|
|
15
|
+
export declare function detectDelimiter(text: string): string;
|
|
16
|
+
export interface ParseDelimitedOptions {
|
|
17
|
+
/** Defaults to {@link detectDelimiter}. */
|
|
18
|
+
delimiter?: string;
|
|
19
|
+
/** Throw `too-many-rows` when the file holds more records than this (header included). */
|
|
20
|
+
maxRecords?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface ParsedRecords {
|
|
23
|
+
delimiter: string;
|
|
24
|
+
/** All non-blank records, header first. */
|
|
25
|
+
records: string[][];
|
|
26
|
+
/** 1-based file line each record starts on. */
|
|
27
|
+
lines: number[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Parse delimited text into records. Handles quoted cells, doubled-quote
|
|
31
|
+
* escapes, embedded delimiters and newlines, CR/LF/CRLF endings, and a BOM.
|
|
32
|
+
* Blank lines are skipped. An unterminated quote is an error, never a
|
|
33
|
+
* silently swallowed remainder of the file.
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseDelimited(input: string, options?: ParseDelimitedOptions): ParsedRecords;
|
|
36
|
+
/**
|
|
37
|
+
* Parse a file into a header row and data rows. Throws `empty` when the file
|
|
38
|
+
* has no header.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseTable(input: string, options?: ParseDelimitedOptions): {
|
|
41
|
+
delimiter: string;
|
|
42
|
+
headers: string[];
|
|
43
|
+
rows: string[][];
|
|
44
|
+
lines: number[];
|
|
45
|
+
};
|
|
46
|
+
export interface FormatDelimitedOptions {
|
|
47
|
+
delimiter?: string;
|
|
48
|
+
eol?: string;
|
|
49
|
+
}
|
|
50
|
+
/** Serialize string cells to delimited text (no trailing newline). */
|
|
51
|
+
export declare function formatDelimited(rows: ReadonlyArray<ReadonlyArray<string>>, options?: FormatDelimitedOptions): string;
|
|
52
|
+
/**
|
|
53
|
+
* Defuse spreadsheet formula injection: a text cell starting with `= + - @`,
|
|
54
|
+
* a tab, or a CR is prefixed with an apostrophe so Excel/Sheets show it as
|
|
55
|
+
* text. Never apply to numeric cells.
|
|
56
|
+
*/
|
|
57
|
+
export declare function neutralizeFormula(value: string): string;
|
|
58
|
+
/** Inverse of {@link neutralizeFormula}, applied to text cells on import. */
|
|
59
|
+
export declare function restoreNeutralized(value: string): string;
|
|
60
|
+
//# sourceMappingURL=csv.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csv.d.ts","sourceRoot":"","sources":["../../../src/components/import-export/csv.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,qBAAa,aAAc,SAAQ,KAAK;IACtC,QAAQ,CAAC,IAAI,EAAE,oBAAoB,GAAG,eAAe,GAAG,OAAO,CAAC;IAChE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEpB,IAAI,EAAE,oBAAoB,GAAG,eAAe,GAAG,OAAO,EACtD,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM;CAOlB;AAID,iEAAiE;AACjE,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA6BpD;AAED,MAAM,WAAW,qBAAqB;IACpC,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0FAA0F;IAC1F,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;IACpB,+CAA+C;IAC/C,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,aAAa,CAqFf;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAW7E;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAaD,sEAAsE;AACtE,wBAAgB,eAAe,CAC7B,IAAI,EAAE,aAAa,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,EAC1C,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAWR;AAID;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,6EAA6E;AAC7E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIxD"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 4180 delimited-text parsing and formatting. Pure; no DOM.
|
|
3
|
+
*/
|
|
4
|
+
export class CsvParseError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
line;
|
|
7
|
+
constructor(code, line, message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'CsvParseError';
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.line = line;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const CANDIDATE_DELIMITERS = [',', ';', '\t', '|'];
|
|
15
|
+
/** Strip a UTF-8 byte-order mark left by spreadsheet exports. */
|
|
16
|
+
export function stripBom(text) {
|
|
17
|
+
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Pick the delimiter that occurs most often, outside quotes, on the first
|
|
21
|
+
* non-empty line. Ties and empty input resolve to a comma.
|
|
22
|
+
*/
|
|
23
|
+
export function detectDelimiter(text) {
|
|
24
|
+
const counts = new Map(CANDIDATE_DELIMITERS.map((d) => [d, 0]));
|
|
25
|
+
let inQuotes = false;
|
|
26
|
+
let sawContent = false;
|
|
27
|
+
for (const ch of stripBom(text)) {
|
|
28
|
+
if (ch === '"') {
|
|
29
|
+
inQuotes = !inQuotes;
|
|
30
|
+
sawContent = true;
|
|
31
|
+
}
|
|
32
|
+
else if (!inQuotes && (ch === '\n' || ch === '\r')) {
|
|
33
|
+
if (sawContent)
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
else if (!inQuotes) {
|
|
37
|
+
sawContent = true;
|
|
38
|
+
const count = counts.get(ch);
|
|
39
|
+
if (count !== undefined)
|
|
40
|
+
counts.set(ch, count + 1);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
sawContent = true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
let best = ',';
|
|
47
|
+
let bestCount = 0;
|
|
48
|
+
for (const [delimiter, count] of counts) {
|
|
49
|
+
if (count > bestCount) {
|
|
50
|
+
best = delimiter;
|
|
51
|
+
bestCount = count;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return best;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Parse delimited text into records. Handles quoted cells, doubled-quote
|
|
58
|
+
* escapes, embedded delimiters and newlines, CR/LF/CRLF endings, and a BOM.
|
|
59
|
+
* Blank lines are skipped. An unterminated quote is an error, never a
|
|
60
|
+
* silently swallowed remainder of the file.
|
|
61
|
+
*/
|
|
62
|
+
export function parseDelimited(input, options = {}) {
|
|
63
|
+
const text = stripBom(input);
|
|
64
|
+
const delimiter = options.delimiter ?? detectDelimiter(text);
|
|
65
|
+
const records = [];
|
|
66
|
+
const lines = [];
|
|
67
|
+
let record = [];
|
|
68
|
+
let cell = '';
|
|
69
|
+
let inQuotes = false;
|
|
70
|
+
let cellQuoted = false;
|
|
71
|
+
let recordQuoted = false;
|
|
72
|
+
let line = 1;
|
|
73
|
+
let recordLine = 1;
|
|
74
|
+
let quoteLine = 1;
|
|
75
|
+
const finishCell = () => {
|
|
76
|
+
record.push(cell);
|
|
77
|
+
cell = '';
|
|
78
|
+
cellQuoted = false;
|
|
79
|
+
};
|
|
80
|
+
const finishRecord = () => {
|
|
81
|
+
finishCell();
|
|
82
|
+
// A line with no cells and no quoted cell is blank; skip it.
|
|
83
|
+
const blank = record.length === 1 && record[0] === '' && !recordQuoted;
|
|
84
|
+
if (!blank) {
|
|
85
|
+
if (options.maxRecords !== undefined &&
|
|
86
|
+
records.length >= options.maxRecords) {
|
|
87
|
+
throw new CsvParseError('too-many-rows', recordLine, `File has more than ${options.maxRecords} records`);
|
|
88
|
+
}
|
|
89
|
+
records.push(record);
|
|
90
|
+
lines.push(recordLine);
|
|
91
|
+
}
|
|
92
|
+
record = [];
|
|
93
|
+
recordQuoted = false;
|
|
94
|
+
};
|
|
95
|
+
for (let i = 0; i < text.length; i++) {
|
|
96
|
+
const ch = text[i];
|
|
97
|
+
if (inQuotes) {
|
|
98
|
+
if (ch === '"') {
|
|
99
|
+
if (text[i + 1] === '"') {
|
|
100
|
+
cell += '"';
|
|
101
|
+
i++;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
inQuotes = false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
if (ch === '\n')
|
|
109
|
+
line++;
|
|
110
|
+
cell += ch;
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (ch === '"' && cell === '' && !cellQuoted) {
|
|
115
|
+
inQuotes = true;
|
|
116
|
+
cellQuoted = true;
|
|
117
|
+
recordQuoted = true;
|
|
118
|
+
quoteLine = line;
|
|
119
|
+
}
|
|
120
|
+
else if (ch === delimiter) {
|
|
121
|
+
finishCell();
|
|
122
|
+
}
|
|
123
|
+
else if (ch === '\n' || ch === '\r') {
|
|
124
|
+
if (ch === '\r' && text[i + 1] === '\n')
|
|
125
|
+
i++;
|
|
126
|
+
finishRecord();
|
|
127
|
+
line++;
|
|
128
|
+
recordLine = line;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
cell += ch;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (inQuotes) {
|
|
135
|
+
throw new CsvParseError('unterminated-quote', quoteLine, `Unterminated quoted cell starting on line ${quoteLine}`);
|
|
136
|
+
}
|
|
137
|
+
if (cell !== '' || record.length > 0 || cellQuoted)
|
|
138
|
+
finishRecord();
|
|
139
|
+
return { delimiter, records, lines };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Parse a file into a header row and data rows. Throws `empty` when the file
|
|
143
|
+
* has no header.
|
|
144
|
+
*/
|
|
145
|
+
export function parseTable(input, options = {}) {
|
|
146
|
+
const { delimiter, records, lines } = parseDelimited(input, options);
|
|
147
|
+
if (records.length === 0) {
|
|
148
|
+
throw new CsvParseError('empty', 1, 'File is empty');
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
delimiter,
|
|
152
|
+
headers: records[0].map((h) => h.trim()),
|
|
153
|
+
rows: records.slice(1),
|
|
154
|
+
lines: lines.slice(1),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function quoteCell(value, delimiter) {
|
|
158
|
+
const needsQuotes = value.includes(delimiter) ||
|
|
159
|
+
value.includes('"') ||
|
|
160
|
+
value.includes('\n') ||
|
|
161
|
+
value.includes('\r') ||
|
|
162
|
+
value.startsWith(' ') ||
|
|
163
|
+
value.endsWith(' ');
|
|
164
|
+
return needsQuotes ? `"${value.replaceAll('"', '""')}"` : value;
|
|
165
|
+
}
|
|
166
|
+
/** Serialize string cells to delimited text (no trailing newline). */
|
|
167
|
+
export function formatDelimited(rows, options = {}) {
|
|
168
|
+
const delimiter = options.delimiter ?? ',';
|
|
169
|
+
const eol = options.eol ?? '\r\n';
|
|
170
|
+
return rows
|
|
171
|
+
.map((row) =>
|
|
172
|
+
// A lone empty cell is quoted so the line is not read back as blank.
|
|
173
|
+
row.length === 1 && row[0] === ''
|
|
174
|
+
? '""'
|
|
175
|
+
: row.map((cell) => quoteCell(cell, delimiter)).join(delimiter))
|
|
176
|
+
.join(eol);
|
|
177
|
+
}
|
|
178
|
+
const FORMULA_TRIGGER = /^[=+\-@\t\r]/;
|
|
179
|
+
/**
|
|
180
|
+
* Defuse spreadsheet formula injection: a text cell starting with `= + - @`,
|
|
181
|
+
* a tab, or a CR is prefixed with an apostrophe so Excel/Sheets show it as
|
|
182
|
+
* text. Never apply to numeric cells.
|
|
183
|
+
*/
|
|
184
|
+
export function neutralizeFormula(value) {
|
|
185
|
+
return FORMULA_TRIGGER.test(value) ? `'${value}` : value;
|
|
186
|
+
}
|
|
187
|
+
/** Inverse of {@link neutralizeFormula}, applied to text cells on import. */
|
|
188
|
+
export function restoreNeutralized(value) {
|
|
189
|
+
return value.startsWith("'") && FORMULA_TRIGGER.test(value.slice(1))
|
|
190
|
+
? value.slice(1)
|
|
191
|
+
: value;
|
|
192
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../../src/components/import-export/download.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAmBnD"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Hand a generated file to the browser as a download. No-op without a DOM. */
|
|
2
|
+
export function downloadFile(file) {
|
|
3
|
+
if (typeof document === 'undefined' ||
|
|
4
|
+
typeof URL.createObjectURL !== 'function') {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
const url = URL.createObjectURL(new Blob([file.content], { type: file.mimeType }));
|
|
8
|
+
const anchor = document.createElement('a');
|
|
9
|
+
anchor.href = url;
|
|
10
|
+
anchor.download = file.filename;
|
|
11
|
+
anchor.rel = 'noopener';
|
|
12
|
+
document.body.append(anchor);
|
|
13
|
+
anchor.click();
|
|
14
|
+
anchor.remove();
|
|
15
|
+
// Revoke after the click has been dispatched so the download can start.
|
|
16
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
17
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ExportFile, ImportExportField } from './types.js';
|
|
2
|
+
export type ExportFormat = 'csv' | 'tsv';
|
|
3
|
+
export interface BuildExportOptions {
|
|
4
|
+
rows: ReadonlyArray<Record<string, unknown>>;
|
|
5
|
+
fields: readonly ImportExportField[];
|
|
6
|
+
/** Field names to export, in order. Defaults to every exportable field. */
|
|
7
|
+
columns?: readonly string[];
|
|
8
|
+
format?: ExportFormat;
|
|
9
|
+
includeHeader?: boolean;
|
|
10
|
+
/** `name` keeps the file re-importable by exact match; `label` is friendlier. */
|
|
11
|
+
headerStyle?: 'name' | 'label';
|
|
12
|
+
/** Defuse spreadsheet formula injection in text cells. Default true. */
|
|
13
|
+
neutralizeFormulas?: boolean;
|
|
14
|
+
/** Prefix a UTF-8 BOM (CSV only) so Excel detects the encoding. Default true. */
|
|
15
|
+
bom?: boolean;
|
|
16
|
+
/** Base file name without extension. Default `export`. */
|
|
17
|
+
filename?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Fields eligible for export, honoring an explicit ordered selection. */
|
|
20
|
+
export declare function selectExportFields(fields: readonly ImportExportField[], columns?: readonly string[]): ImportExportField[];
|
|
21
|
+
/** Render one record value as cell text. */
|
|
22
|
+
export declare function serializeCell(value: unknown): string;
|
|
23
|
+
/** Build a downloadable CSV/TSV file from loaded rows. Pure. */
|
|
24
|
+
export declare function buildExportFile(options: BuildExportOptions): ExportFile;
|
|
25
|
+
//# sourceMappingURL=export.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../../src/components/import-export/export.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEhE,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,KAAK,CAAC;AAEzC,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC7C,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACrC,2EAA2E;IAC3E,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,wEAAwE;IACxE,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,iFAAiF;IACjF,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAID,0EAA0E;AAC1E,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,SAAS,iBAAiB,EAAE,EACpC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,GAC1B,iBAAiB,EAAE,CAQrB;AAED,4CAA4C;AAC5C,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAcpD;AAOD,gEAAgE;AAChE,wBAAgB,eAAe,CAAC,OAAO,EAAE,kBAAkB,GAAG,UAAU,CAqCvE"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { formatDelimited, neutralizeFormula } from './csv.js';
|
|
2
|
+
const NUMERIC_TYPES = new Set(['integer', 'decimal', 'boolean']);
|
|
3
|
+
/** Fields eligible for export, honoring an explicit ordered selection. */
|
|
4
|
+
export function selectExportFields(fields, columns) {
|
|
5
|
+
const eligible = fields.filter((f) => f.exportable);
|
|
6
|
+
if (!columns)
|
|
7
|
+
return eligible;
|
|
8
|
+
const byName = new Map(eligible.map((f) => [f.name, f]));
|
|
9
|
+
return columns.flatMap((name) => {
|
|
10
|
+
const field = byName.get(name);
|
|
11
|
+
return field ? [field] : [];
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
/** Render one record value as cell text. */
|
|
15
|
+
export function serializeCell(value) {
|
|
16
|
+
if (value === null || value === undefined)
|
|
17
|
+
return '';
|
|
18
|
+
if (typeof value === 'string')
|
|
19
|
+
return value;
|
|
20
|
+
if (typeof value === 'number' || typeof value === 'bigint')
|
|
21
|
+
return String(value);
|
|
22
|
+
if (typeof value === 'boolean')
|
|
23
|
+
return value ? 'true' : 'false';
|
|
24
|
+
if (value instanceof Date) {
|
|
25
|
+
return Number.isNaN(value.getTime()) ? '' : value.toISOString();
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
return JSON.stringify(value) ?? '';
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function safeFilename(base) {
|
|
35
|
+
const cleaned = base.replace(/[^\w.-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
36
|
+
return cleaned || 'export';
|
|
37
|
+
}
|
|
38
|
+
/** Build a downloadable CSV/TSV file from loaded rows. Pure. */
|
|
39
|
+
export function buildExportFile(options) {
|
|
40
|
+
const format = options.format ?? 'csv';
|
|
41
|
+
const selected = selectExportFields(options.fields, options.columns);
|
|
42
|
+
const guard = options.neutralizeFormulas !== false;
|
|
43
|
+
const delimiter = format === 'tsv' ? '\t' : ',';
|
|
44
|
+
const table = [];
|
|
45
|
+
if (options.includeHeader !== false) {
|
|
46
|
+
table.push(selected.map((f) => (options.headerStyle === 'label' ? f.label : f.name)));
|
|
47
|
+
}
|
|
48
|
+
for (const row of options.rows) {
|
|
49
|
+
table.push(selected.map((field) => {
|
|
50
|
+
const text = serializeCell(row[field.name]);
|
|
51
|
+
// Numbers and booleans keep their sign; only free text can carry a formula.
|
|
52
|
+
return guard && !NUMERIC_TYPES.has(field.type)
|
|
53
|
+
? neutralizeFormula(text)
|
|
54
|
+
: text;
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
const body = selected.length === 0 ? '' : formatDelimited(table, { delimiter });
|
|
58
|
+
const withBom = format === 'csv' && options.bom !== false;
|
|
59
|
+
return {
|
|
60
|
+
filename: `${safeFilename(options.filename ?? 'export')}.${format}`,
|
|
61
|
+
mimeType: format === 'tsv'
|
|
62
|
+
? 'text/tab-separated-values;charset=utf-8'
|
|
63
|
+
: 'text/csv;charset=utf-8',
|
|
64
|
+
content: `${withBom ? '' : ''}${body}`,
|
|
65
|
+
rowCount: options.rows.length,
|
|
66
|
+
columnCount: selected.length,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { SmrtWebCollectionDefinition } from '@happyvertical/smrt-web';
|
|
2
|
+
import type { ImportExportField } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The slice of a resolved field policy (`ResolvedFieldPolicy` from
|
|
5
|
+
* `@happyvertical/smrt-fields`) the import/export layer reads. Declared
|
|
6
|
+
* structurally so this package takes no dependency on smrt-fields: pass the
|
|
7
|
+
* resolver's `fields` record as-is.
|
|
8
|
+
*/
|
|
9
|
+
export interface ImportExportFieldPolicy {
|
|
10
|
+
visibility?: 'basic' | 'advanced' | 'hidden';
|
|
11
|
+
label?: string | null;
|
|
12
|
+
help?: string | null;
|
|
13
|
+
order?: number | null;
|
|
14
|
+
/** An org-locked field keeps its resolved default and is never overwritten by import. */
|
|
15
|
+
locked?: boolean;
|
|
16
|
+
hasDefault?: boolean;
|
|
17
|
+
defaultValue?: unknown;
|
|
18
|
+
required?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface FieldsFromDefinitionOptions {
|
|
21
|
+
/** Resolved policy per field name (`ResolvedObjectFieldPolicy.fields`). */
|
|
22
|
+
policy?: Readonly<Record<string, ImportExportFieldPolicy>>;
|
|
23
|
+
/** Enumerations the manifest does not carry: field name to allowed values. */
|
|
24
|
+
enums?: Readonly<Record<string, readonly string[]>>;
|
|
25
|
+
/** Field names that must be unique within an imported file. */
|
|
26
|
+
unique?: readonly string[];
|
|
27
|
+
/** Field names to leave out entirely. */
|
|
28
|
+
exclude?: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
/** `publishedAt` becomes `Published At`. */
|
|
31
|
+
export declare function humanizeFieldName(name: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Derive import/export columns from a generated collection definition (the
|
|
34
|
+
* manifest) and, optionally, the resolved field policy:
|
|
35
|
+
*
|
|
36
|
+
* - system fields (`createdAt`, `updatedAt`, ...) and `_`-prefixed storage
|
|
37
|
+
* fields are skipped; `id` is exportable but never imported; tenant fields
|
|
38
|
+
* are neither
|
|
39
|
+
* - a supplied policy is an allowlist: a field the policy omits is neither
|
|
40
|
+
* imported nor exported (manifest defaults apply only with no policy)
|
|
41
|
+
* - a policy `hidden` field is neither imported nor exported, so an export
|
|
42
|
+
* cannot reveal what the UI hides
|
|
43
|
+
* - a policy `locked` field is not importable and keeps its resolved default
|
|
44
|
+
* - policy label, help, order, default and required override the manifest seed
|
|
45
|
+
* - order is policy order, then `ui.order`, then manifest declaration order
|
|
46
|
+
*/
|
|
47
|
+
export declare function fieldsFromCollectionDefinition(definition: Pick<SmrtWebCollectionDefinition, 'fields'>, options?: FieldsFromDefinitionOptions): ImportExportField[];
|
|
48
|
+
//# sourceMappingURL=fields.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fields.d.ts","sourceRoot":"","sources":["../../../src/components/import-export/fields.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,2BAA2B,EAE5B,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,iBAAiB,EAAyB,MAAM,YAAY,CAAC;AAE3E;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACtC,UAAU,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAC;IAC7C,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,yFAAyF;IACzF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,2BAA2B;IAC1C,2EAA2E;IAC3E,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC,CAAC;IAC3D,8EAA8E;IAC9E,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;IACpD,+DAA+D;IAC/D,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,yCAAyC;IACzC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAyBD,4CAA4C;AAC5C,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMtD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,8BAA8B,CAC5C,UAAU,EAAE,IAAI,CAAC,2BAA2B,EAAE,QAAQ,CAAC,EACvD,OAAO,GAAE,2BAAgC,GACxC,iBAAiB,EAAE,CAoErB"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/** Managed by the framework or the server; never imported or exported by default. */
|
|
2
|
+
const SYSTEM_FIELDS = new Set([
|
|
3
|
+
'createdAt',
|
|
4
|
+
'updatedAt',
|
|
5
|
+
'created_at',
|
|
6
|
+
'updated_at',
|
|
7
|
+
'context',
|
|
8
|
+
]);
|
|
9
|
+
/** Set from the session by tenant-scoped collections. */
|
|
10
|
+
const TENANT_FIELDS = new Set(['tenantId', 'tenant_id']);
|
|
11
|
+
const TYPE_MAP = {
|
|
12
|
+
text: 'text',
|
|
13
|
+
decimal: 'decimal',
|
|
14
|
+
integer: 'integer',
|
|
15
|
+
boolean: 'boolean',
|
|
16
|
+
datetime: 'datetime',
|
|
17
|
+
json: 'json',
|
|
18
|
+
foreignKey: 'reference',
|
|
19
|
+
crossPackageRef: 'reference',
|
|
20
|
+
};
|
|
21
|
+
/** `publishedAt` becomes `Published At`. */
|
|
22
|
+
export function humanizeFieldName(name) {
|
|
23
|
+
const spaced = name
|
|
24
|
+
.replace(/[_-]+/g, ' ')
|
|
25
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
26
|
+
.trim();
|
|
27
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Derive import/export columns from a generated collection definition (the
|
|
31
|
+
* manifest) and, optionally, the resolved field policy:
|
|
32
|
+
*
|
|
33
|
+
* - system fields (`createdAt`, `updatedAt`, ...) and `_`-prefixed storage
|
|
34
|
+
* fields are skipped; `id` is exportable but never imported; tenant fields
|
|
35
|
+
* are neither
|
|
36
|
+
* - a supplied policy is an allowlist: a field the policy omits is neither
|
|
37
|
+
* imported nor exported (manifest defaults apply only with no policy)
|
|
38
|
+
* - a policy `hidden` field is neither imported nor exported, so an export
|
|
39
|
+
* cannot reveal what the UI hides
|
|
40
|
+
* - a policy `locked` field is not importable and keeps its resolved default
|
|
41
|
+
* - policy label, help, order, default and required override the manifest seed
|
|
42
|
+
* - order is policy order, then `ui.order`, then manifest declaration order
|
|
43
|
+
*/
|
|
44
|
+
export function fieldsFromCollectionDefinition(definition, options = {}) {
|
|
45
|
+
const { policy, enums = {}, unique = [], exclude = [] } = options;
|
|
46
|
+
const skip = new Set(exclude);
|
|
47
|
+
const uniqueSet = new Set(unique);
|
|
48
|
+
const entries = [];
|
|
49
|
+
let seq = 0;
|
|
50
|
+
for (const [name, def] of Object.entries(definition.fields)) {
|
|
51
|
+
seq++;
|
|
52
|
+
if (name.startsWith('_') || SYSTEM_FIELDS.has(name) || skip.has(name))
|
|
53
|
+
continue;
|
|
54
|
+
if (name === 'id')
|
|
55
|
+
continue; // re-added below with fixed semantics
|
|
56
|
+
const p = policy?.[name];
|
|
57
|
+
// A supplied resolved policy is an allowlist: the resolver omits sensitive,
|
|
58
|
+
// transient and read-gated fields, so a field absent from it never appears.
|
|
59
|
+
if (policy && !p)
|
|
60
|
+
continue;
|
|
61
|
+
const tenant = TENANT_FIELDS.has(name);
|
|
62
|
+
const hidden = p?.visibility === 'hidden';
|
|
63
|
+
const locked = p?.locked === true;
|
|
64
|
+
const enumValues = enums[name];
|
|
65
|
+
const hasDefault = p?.hasDefault === true ||
|
|
66
|
+
(p?.hasDefault === undefined && def.default !== undefined);
|
|
67
|
+
const defaultValue = p?.hasDefault === true ? p.defaultValue : def.default;
|
|
68
|
+
const required = p?.required ?? (def.required === true && def.nullable !== true);
|
|
69
|
+
entries.push({
|
|
70
|
+
field: {
|
|
71
|
+
name,
|
|
72
|
+
label: p?.label || humanizeFieldName(name),
|
|
73
|
+
type: enumValues ? 'enum' : TYPE_MAP[def.type],
|
|
74
|
+
required: required && !hasDefault,
|
|
75
|
+
importable: !tenant && !hidden && !locked,
|
|
76
|
+
exportable: !tenant && !hidden,
|
|
77
|
+
...(enumValues ? { options: enumValues } : {}),
|
|
78
|
+
...(def.ui?.widget ? { widget: def.ui.widget } : {}),
|
|
79
|
+
...((p?.help ?? def.description)
|
|
80
|
+
? { help: (p?.help ?? def.description) }
|
|
81
|
+
: {}),
|
|
82
|
+
...(hasDefault ? { hasDefault: true, defaultValue } : {}),
|
|
83
|
+
...(locked && !hidden && !tenant ? { locked: true } : {}),
|
|
84
|
+
...(uniqueSet.has(name) ? { unique: true } : {}),
|
|
85
|
+
},
|
|
86
|
+
order: p?.order ?? def.ui?.order ?? Number.POSITIVE_INFINITY,
|
|
87
|
+
seq,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
entries.sort((a, b) => a.order === b.order ? a.seq - b.seq : a.order - b.order);
|
|
91
|
+
const fields = entries.map((e) => e.field);
|
|
92
|
+
if (!skip.has('id')) {
|
|
93
|
+
fields.unshift({
|
|
94
|
+
name: 'id',
|
|
95
|
+
label: 'ID',
|
|
96
|
+
type: 'reference',
|
|
97
|
+
required: false,
|
|
98
|
+
importable: false,
|
|
99
|
+
exportable: true,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return fields;
|
|
103
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { type CollectionImportExport, createCollectionImportExport, ExportLimitError, type LoadRowsOptions, } from './collection-adapter.js';
|
|
2
|
+
export { CsvParseError, detectDelimiter, formatDelimited, neutralizeFormula, parseDelimited, parseTable, restoreNeutralized, stripBom, } from './csv.js';
|
|
3
|
+
export { type BuildExportOptions, buildExportFile, type ExportFormat, selectExportFields, serializeCell, } from './export.js';
|
|
4
|
+
export { type FieldsFromDefinitionOptions, fieldsFromCollectionDefinition, humanizeFieldName, type ImportExportFieldPolicy, } from './fields.js';
|
|
5
|
+
export { default as ImportExport } from './ImportExport.svelte';
|
|
6
|
+
export { autoMapColumns, normalizeHeader, setColumnTarget } from './mapping.js';
|
|
7
|
+
export type { ImportExportProps } from './props.js';
|
|
8
|
+
export { buildErrorReport, DEFAULT_ISSUE_MESSAGES, describeIssue, type IssueMessages, } from './report.js';
|
|
9
|
+
export { type RunImportOptions, runImport } from './runner.js';
|
|
10
|
+
export type { ColumnMapping, ExportFile, ImportExportField, ImportExportFieldType, ImportExportWidget, ImportIssue, ImportIssueCode, ImportRunResult, ParsedTable, ValidationResult, ValidRecord, } from './types.js';
|
|
11
|
+
export { type CoerceResult, coerceCell, type ValidateRowsInput, validateMapping, validateRows, } from './validate.js';
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/import-export/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,sBAAsB,EAC3B,4BAA4B,EAC5B,gBAAgB,EAChB,KAAK,eAAe,GACrB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,aAAa,EACb,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,UAAU,EACV,kBAAkB,EAClB,QAAQ,GACT,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,KAAK,kBAAkB,EACvB,eAAe,EACf,KAAK,YAAY,EACjB,kBAAkB,EAClB,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,KAAK,2BAA2B,EAChC,8BAA8B,EAC9B,iBAAiB,EACjB,KAAK,uBAAuB,GAC7B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAChF,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,aAAa,EACb,KAAK,aAAa,GACnB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,KAAK,gBAAgB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC/D,YAAY,EACV,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,eAAe,EACf,WAAW,EACX,gBAAgB,EAChB,WAAW,GACZ,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,KAAK,YAAY,EACjB,UAAU,EACV,KAAK,iBAAiB,EACtB,eAAe,EACf,YAAY,GACb,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { createCollectionImportExport, ExportLimitError, } from './collection-adapter.js';
|
|
2
|
+
export { CsvParseError, detectDelimiter, formatDelimited, neutralizeFormula, parseDelimited, parseTable, restoreNeutralized, stripBom, } from './csv.js';
|
|
3
|
+
export { buildExportFile, selectExportFields, serializeCell, } from './export.js';
|
|
4
|
+
export { fieldsFromCollectionDefinition, humanizeFieldName, } from './fields.js';
|
|
5
|
+
export { default as ImportExport } from './ImportExport.svelte';
|
|
6
|
+
export { autoMapColumns, normalizeHeader, setColumnTarget } from './mapping.js';
|
|
7
|
+
export { buildErrorReport, DEFAULT_ISSUE_MESSAGES, describeIssue, } from './report.js';
|
|
8
|
+
export { runImport } from './runner.js';
|
|
9
|
+
export { coerceCell, validateMapping, validateRows, } from './validate.js';
|