@svgrid/enterprise 2.3.0 → 2.3.2

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 (73) hide show
  1. package/dist/cdn/{pdfmake-C6pyKjX0.js → pdfmake-DVbZqKeD.js} +2297 -2327
  2. package/dist/cdn/rolldown-runtime-DtPi1Y-2.js +13 -0
  3. package/dist/cdn/{smart.export-CfW2zpyq.js → smart.export-D9FF4wAB.js} +54 -51
  4. package/dist/cdn/svgrid-enterprise.svelte-external.js +11308 -7898
  5. package/dist/cdn/{vfs_fonts-BktEULEs.js → vfs_fonts-iHMTEkpG.js} +1 -1
  6. package/dist/designer/assets/GridMenus-CW-rnHS9.js +7 -0
  7. package/dist/designer/assets/SvDateTimePicker-Cygij-dr.css +1 -0
  8. package/dist/designer/assets/SvDateTimePicker-DOBYyLeN.js +1 -0
  9. package/dist/designer/assets/SvGridChart-BEDNTdc5.js +2 -0
  10. package/dist/designer/assets/SvGridChart-Vu4Nvjiv.css +1 -0
  11. package/dist/designer/assets/SvGridChartPanel-x3lzSxuf.js +10 -0
  12. package/dist/designer/assets/SvGridChartView-CAKhPELi.js +1 -0
  13. package/dist/designer/assets/SvGridChartView-DX9HfkBR.css +1 -0
  14. package/dist/designer/assets/SvGridDropdown-CAYG7Y-O.js +1 -0
  15. package/dist/designer/assets/SvGridDropdown-Crwb2P3Q.css +1 -0
  16. package/dist/designer/assets/SvListBox-BMSfFJJT.css +1 -0
  17. package/dist/designer/assets/SvListBox-BVzVfCG3.js +1 -0
  18. package/dist/designer/assets/chart-DHEgt4lN.js +1 -0
  19. package/dist/designer/assets/disclose-version-CIm4S9xS.js +2 -0
  20. package/dist/designer/assets/dismissable-2EZKoens.js +1 -0
  21. package/dist/designer/assets/dismissable-BF-9TJSd.css +1 -0
  22. package/dist/designer/assets/easing-zaypKrr0.js +3 -0
  23. package/dist/designer/assets/export-format-DwjuTfrQ.js +6 -0
  24. package/dist/designer/assets/index-DVrv9dH6.js +75797 -0
  25. package/dist/designer/assets/index-DjPp1mn7.css +1 -0
  26. package/dist/designer/assets/js-scroller.svelte-tHanKJx0.js +110 -0
  27. package/dist/designer/assets/jszip.min-CX2fPs3O.js +2 -0
  28. package/dist/designer/assets/pdfmake-BpIYFHoG.js +242 -0
  29. package/dist/designer/assets/popover-CUkLsvRX.js +1 -0
  30. package/dist/designer/assets/rolldown-runtime-Dd_uD5pT.js +1 -0
  31. package/dist/designer/assets/smart.export-CDCwbFH4.js +37 -0
  32. package/dist/designer/assets/src-BRKu53tV.js +2842 -0
  33. package/dist/designer/assets/src-DseB1VRN.css +1 -0
  34. package/dist/designer/assets/vfs_fonts-BV-Ul9v2.js +1 -0
  35. package/dist/designer/index.html +32 -0
  36. package/dist/node/studio.js +16778 -12672
  37. package/package.json +10 -7
  38. package/src/SvGridBoard.svelte +4 -1
  39. package/src/SvGridScheduler.svelte +114 -90
  40. package/src/ai-export-pdf.dom.test.ts +77 -77
  41. package/src/ai-export-xlsx.dom.test.ts +90 -90
  42. package/src/ai-export.dom.test.ts +114 -114
  43. package/src/export-ooxml.ts +4 -0
  44. package/src/export-xls.ts +3 -0
  45. package/src/expressions/evaluate.ts +9 -0
  46. package/src/import.test.ts +1 -1
  47. package/src/import.ts +1 -1
  48. package/src/index.ts +12 -0
  49. package/src/pivot.test.ts +0 -1
  50. package/src/schema-designer.ts +1 -1
  51. package/src/sources/introspect-supabase.ts +2 -2
  52. package/src/studio/emit-project.test.ts +298 -7
  53. package/src/studio/emit-project.ts +484 -30
  54. package/src/studio/emit-schema.ts +20 -10
  55. package/src/studio/index.ts +34 -0
  56. package/src/studio/init-flow.test.ts +364 -0
  57. package/src/studio/init-flow.ts +409 -0
  58. package/src/studio/project.test.ts +0 -1
  59. package/src/studio/project.ts +109 -2
  60. package/src/studio/samples/datasets.test.ts +84 -0
  61. package/src/studio/samples/datasets.ts +340 -0
  62. package/src/studio/samples/live-data.test.ts +1 -1
  63. package/src/studio/samples/samples.test.ts +268 -268
  64. package/src/studio/samples/shared.ts +7 -150
  65. package/src/studio/screen-suites.test.ts +226 -0
  66. package/src/studio/screen-suites.ts +445 -0
  67. package/src/studio/ui-components-surface.test.ts +2 -2
  68. package/src/studio/ui-components.generated.ts +1643 -106
  69. package/src/studio/ui-components.ts +742 -641
  70. package/src/sveltekit/index.ts +1 -0
  71. package/src/sveltekit/sql-source.ts +1 -1
  72. package/src/sveltekit/transport-scope.test.ts +125 -0
  73. package/src/sveltekit/transport.ts +76 -2
@@ -1,90 +1,90 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest'
2
-
3
- // Real exportGrid runs (NOT mocked). Capture the xlsx parts / avoid the DOM.
4
- const captured: { parts: Record<string, string> } = { parts: {} }
5
- vi.mock('jszip', () => ({
6
- default: class {
7
- file(p: string, d: string) {
8
- captured.parts[p] = d
9
- }
10
- async generateAsync() {
11
- return new Blob([])
12
- }
13
- },
14
- }))
15
- vi.mock('./export-serialize', async (importOriginal) => {
16
- const actual = (await importOriginal()) as Record<string, unknown>
17
- return { ...actual, downloadBlobFile: () => {}, downloadTextFile: () => {} }
18
- })
19
-
20
- import { aiExport, setAIProvider, mockAIProvider, registerExportProvider } from '@svgrid/grid'
21
- import { exportGrid } from './export'
22
- import { setLicenseKey } from './license'
23
-
24
- // Mirrors the real makeOrders shape - crucially includes `inStock` (boolean)
25
- // and `quantity`/`index` (numbers that never exceed 300), so the mock must
26
- // target `price` for "$300", not a boolean or a count.
27
- type Order = {
28
- id: string
29
- index: number
30
- country: string
31
- company: string
32
- product: string
33
- inStock: boolean
34
- quantity: number
35
- price: number
36
- }
37
- const rows: Order[] = Array.from({ length: 120 }, (_, i) => ({
38
- id: `o${i}`,
39
- index: i + 1,
40
- country: ['USA', 'UK', 'Germany'][i % 3]!,
41
- company: `Co${i}`,
42
- product: 'Widget',
43
- inStock: i % 3 !== 0,
44
- quantity: 5 + (i % 50), // max 54 - never > 300
45
- price: Math.round((10 + ((i * 41) % 480)) * 100) / 100, // spread 10..490
46
- }))
47
-
48
- function makeApi() {
49
- return {
50
- getData: () => rows,
51
- getDisplayedRows: () => rows,
52
- getSelectedRows: () => [],
53
- getColumns: () => [
54
- { id: 'country', field: 'country', header: 'Country', visible: true },
55
- { id: 'company', field: 'company', header: 'Company', visible: true },
56
- { id: 'product', field: 'product', header: 'Product', visible: true },
57
- { id: 'quantity', field: 'quantity', header: 'Qty', visible: true },
58
- { id: 'price', field: 'price', header: 'Price', visible: true, format: { type: 'currency', currency: 'USD' } },
59
- ],
60
- getState: () => ({ grouping: [] }),
61
- getColumnPinning: () => ({ left: [], right: [] }),
62
- clearAllFilters: vi.fn(),
63
- clearSort: vi.fn(),
64
- setFilter: vi.fn(),
65
- setSort: vi.fn(),
66
- setGroupBy: vi.fn(),
67
- } as any
68
- }
69
-
70
- beforeEach(() => {
71
- captured.parts = {}
72
- setLicenseKey('SVENTERPRISE-DEV-TEST')
73
- setAIProvider(mockAIProvider)
74
- // aiExport is free (grid) and delegates the real write to enterprise's engine.
75
- registerExportProvider(exportGrid)
76
- })
77
-
78
- describe('aiExport - "export orders over $300" (xlsx default)', () => {
79
- it('produces a non-empty xlsx via the real export path', async () => {
80
- const plan = await aiExport(makeApi(), 'export orders over $300')
81
- expect(plan.format).toBe('xlsx')
82
- expect(plan.filters).toContainEqual({ field: 'price', operator: 'greaterThan', value: '300' })
83
-
84
- const sheet = captured.parts['xl/worksheets/sheet1.xml']
85
- expect(sheet).toBeTruthy()
86
- // header row + at least one data row
87
- const rowCount = (sheet!.match(/<row /g) ?? []).length
88
- expect(rowCount).toBeGreaterThan(1)
89
- })
90
- })
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ // Real exportGrid runs (NOT mocked). Capture the xlsx parts / avoid the DOM.
4
+ const captured: { parts: Record<string, string> } = { parts: {} }
5
+ vi.mock('jszip', () => ({
6
+ default: class {
7
+ file(p: string, d: string) {
8
+ captured.parts[p] = d
9
+ }
10
+ async generateAsync() {
11
+ return new Blob([])
12
+ }
13
+ },
14
+ }))
15
+ vi.mock('./export-serialize', async (importOriginal) => {
16
+ const actual = (await importOriginal()) as Record<string, unknown>
17
+ return { ...actual, downloadBlobFile: () => {}, downloadTextFile: () => {} }
18
+ })
19
+
20
+ import { aiExport, setAIProvider, mockAIProvider, registerExportProvider } from '@svgrid/grid'
21
+ import { exportGrid } from './export'
22
+ import { setLicenseKey } from './license'
23
+
24
+ // Mirrors the real makeOrders shape - crucially includes `inStock` (boolean)
25
+ // and `quantity`/`index` (numbers that never exceed 300), so the mock must
26
+ // target `price` for "$300", not a boolean or a count.
27
+ type Order = {
28
+ id: string
29
+ index: number
30
+ country: string
31
+ company: string
32
+ product: string
33
+ inStock: boolean
34
+ quantity: number
35
+ price: number
36
+ }
37
+ const rows: Order[] = Array.from({ length: 120 }, (_, i) => ({
38
+ id: `o${i}`,
39
+ index: i + 1,
40
+ country: ['USA', 'UK', 'Germany'][i % 3]!,
41
+ company: `Co${i}`,
42
+ product: 'Widget',
43
+ inStock: i % 3 !== 0,
44
+ quantity: 5 + (i % 50), // max 54 - never > 300
45
+ price: Math.round((10 + ((i * 41) % 480)) * 100) / 100, // spread 10..490
46
+ }))
47
+
48
+ function makeApi() {
49
+ return {
50
+ getData: () => rows,
51
+ getDisplayedRows: () => rows,
52
+ getSelectedRows: () => [],
53
+ getColumns: () => [
54
+ { id: 'country', field: 'country', header: 'Country', visible: true },
55
+ { id: 'company', field: 'company', header: 'Company', visible: true },
56
+ { id: 'product', field: 'product', header: 'Product', visible: true },
57
+ { id: 'quantity', field: 'quantity', header: 'Qty', visible: true },
58
+ { id: 'price', field: 'price', header: 'Price', visible: true, format: { type: 'currency', currency: 'USD' } },
59
+ ],
60
+ getState: () => ({ grouping: [] }),
61
+ getColumnPinning: () => ({ left: [], right: [] }),
62
+ clearAllFilters: vi.fn(),
63
+ clearSort: vi.fn(),
64
+ setFilter: vi.fn(),
65
+ setSort: vi.fn(),
66
+ setGroupBy: vi.fn(),
67
+ } as any
68
+ }
69
+
70
+ beforeEach(() => {
71
+ captured.parts = {}
72
+ setLicenseKey('SVENTERPRISE-DEV-TEST')
73
+ setAIProvider(mockAIProvider)
74
+ // aiExport is free (grid) and delegates the real write to enterprise's engine.
75
+ registerExportProvider(exportGrid as never)
76
+ })
77
+
78
+ describe('aiExport - "export orders over $300" (xlsx default)', () => {
79
+ it('produces a non-empty xlsx via the real export path', async () => {
80
+ const plan = await aiExport(makeApi(), 'export orders over $300')
81
+ expect(plan.format).toBe('xlsx')
82
+ expect(plan.filters).toContainEqual({ field: 'price', operator: 'greaterThan', value: '300' })
83
+
84
+ const sheet = captured.parts['xl/worksheets/sheet1.xml']
85
+ expect(sheet).toBeTruthy()
86
+ // header row + at least one data row
87
+ const rowCount = (sheet!.match(/<row /g) ?? []).length
88
+ expect(rowCount).toBeGreaterThan(1)
89
+ })
90
+ })
@@ -1,114 +1,114 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
-
3
- // Capture downloads instead of touching the DOM (aiExport -> exportGrid).
4
- const downloads: Array<{ text: string; filename: string }> = []
5
- vi.mock('./export-serialize', async (importOriginal) => {
6
- const actual = (await importOriginal()) as Record<string, unknown>
7
- return {
8
- ...actual,
9
- downloadTextFile: (text: string, filename: string) => {
10
- downloads.push({ text, filename })
11
- },
12
- }
13
- })
14
-
15
- import { aiExport, aiFindAnomalies, setAIProvider, mockAIProvider, registerExportProvider } from '@svgrid/grid'
16
- import { exportGrid } from './export'
17
- import { setLicenseKey } from './license'
18
-
19
- type Row = { region: string; amount: number; name: string }
20
- const rows: Row[] = [
21
- { region: 'EMEA', amount: 100, name: 'A' },
22
- { region: 'NA', amount: 2000, name: 'B' },
23
- { region: 'EMEA', amount: 50, name: 'C' },
24
- ]
25
-
26
- function makeApi() {
27
- return {
28
- getData: () => rows,
29
- getDisplayedRows: () => rows,
30
- getSelectedRows: () => [],
31
- getColumns: () => [
32
- { id: 'region', field: 'region', header: 'Region', visible: true },
33
- { id: 'amount', field: 'amount', header: 'Amount', visible: true },
34
- { id: 'name', field: 'name', header: 'Name', visible: true },
35
- ],
36
- getState: () => ({ grouping: [] }),
37
- clearAllFilters: vi.fn(),
38
- clearSort: vi.fn(),
39
- setFilter: vi.fn(),
40
- setSort: vi.fn(),
41
- } as any
42
- }
43
-
44
- beforeEach(() => {
45
- downloads.length = 0
46
- setLicenseKey('SVENTERPRISE-DEV-TEST')
47
- setAIProvider(mockAIProvider)
48
- // aiExport is free (grid) and delegates the WRITE to the registered engine;
49
- // register enterprise's exportGrid so the download path actually runs.
50
- registerExportProvider(exportGrid)
51
- })
52
- afterEach(() => setAIProvider(null))
53
-
54
- describe('aiExport', () => {
55
- it('parses format + groupBy + filters; applies to the grid only when asked', async () => {
56
- const api = makeApi()
57
- const plan = await aiExport(api, 'export deals over 500 grouped by region as a pdf', {
58
- run: false,
59
- apply: true,
60
- })
61
- expect(plan.format).toBe('pdf')
62
- expect(plan.groupBy).toEqual(['region'])
63
- expect(plan.filters).toContainEqual({ field: 'amount', operator: 'greaterThan', value: '500' })
64
- // apply: true -> the grid is mutated
65
- expect(api.clearAllFilters).toHaveBeenCalled()
66
- expect(api.setFilter).toHaveBeenCalledWith('amount', { operator: 'greaterThan', value: '500' })
67
- // run:false -> nothing downloaded
68
- expect(downloads).toHaveLength(0)
69
- })
70
-
71
- it('defaults to xlsx and does NOT touch the grid (apply defaults off)', async () => {
72
- const api = makeApi()
73
- const plan = await aiExport(api, 'just export everything', { run: false })
74
- expect(plan.format).toBe('xlsx')
75
- expect(api.clearAllFilters).not.toHaveBeenCalled()
76
- expect(api.setFilter).not.toHaveBeenCalled()
77
- })
78
-
79
- it('runs the export (downloads) when run is not false', async () => {
80
- const api = makeApi()
81
- await aiExport(api, 'export as csv', { filename: 'report' })
82
- expect(downloads).toHaveLength(1)
83
- expect(downloads[0]!.filename).toBe('report.csv')
84
- })
85
-
86
- it('exports the plan-filtered rows directly (not the grid displayed rows)', async () => {
87
- // getDisplayedRows returns EVERYTHING (as if the grid hadn't re-filtered);
88
- // the export must still contain only rows matching the plan's filter.
89
- const api = makeApi()
90
- await aiExport(api, 'export deals over 500 as csv', { filename: 'big' })
91
- const { text } = downloads[0]!
92
- expect(text).toContain('2000') // amount 2000 > 500 kept
93
- expect(text).not.toContain('100') // amount 100 filtered out
94
- expect(text).not.toContain('50') // amount 50 filtered out
95
- })
96
- })
97
-
98
- describe('aiFindAnomalies', () => {
99
- it('flags a numeric outlier over the scanned rows', async () => {
100
- const api = makeApi()
101
- const result = await aiFindAnomalies(api, {})
102
- expect(result.anomalies.length).toBeGreaterThan(0)
103
- const amountAnomaly = result.anomalies.find((a) => a.field === 'amount')
104
- expect(amountAnomaly?.value).toBe(2000) // the max in the sample
105
- expect(amountAnomaly?.severity).toBe('medium')
106
- expect(result.summary).toBeTruthy()
107
- })
108
-
109
- it('returns empty for an empty selection', async () => {
110
- const api = makeApi()
111
- const result = await aiFindAnomalies(api, { target: { kind: 'selection', rowIndices: [] } })
112
- expect(result.anomalies).toEqual([])
113
- })
114
- })
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ // Capture downloads instead of touching the DOM (aiExport -> exportGrid).
4
+ const downloads: Array<{ text: string; filename: string }> = []
5
+ vi.mock('./export-serialize', async (importOriginal) => {
6
+ const actual = (await importOriginal()) as Record<string, unknown>
7
+ return {
8
+ ...actual,
9
+ downloadTextFile: (text: string, filename: string) => {
10
+ downloads.push({ text, filename })
11
+ },
12
+ }
13
+ })
14
+
15
+ import { aiExport, aiFindAnomalies, setAIProvider, mockAIProvider, registerExportProvider } from '@svgrid/grid'
16
+ import { exportGrid } from './export'
17
+ import { setLicenseKey } from './license'
18
+
19
+ type Row = { region: string; amount: number; name: string }
20
+ const rows: Row[] = [
21
+ { region: 'EMEA', amount: 100, name: 'A' },
22
+ { region: 'NA', amount: 2000, name: 'B' },
23
+ { region: 'EMEA', amount: 50, name: 'C' },
24
+ ]
25
+
26
+ function makeApi() {
27
+ return {
28
+ getData: () => rows,
29
+ getDisplayedRows: () => rows,
30
+ getSelectedRows: () => [],
31
+ getColumns: () => [
32
+ { id: 'region', field: 'region', header: 'Region', visible: true },
33
+ { id: 'amount', field: 'amount', header: 'Amount', visible: true },
34
+ { id: 'name', field: 'name', header: 'Name', visible: true },
35
+ ],
36
+ getState: () => ({ grouping: [] }),
37
+ clearAllFilters: vi.fn(),
38
+ clearSort: vi.fn(),
39
+ setFilter: vi.fn(),
40
+ setSort: vi.fn(),
41
+ } as any
42
+ }
43
+
44
+ beforeEach(() => {
45
+ downloads.length = 0
46
+ setLicenseKey('SVENTERPRISE-DEV-TEST')
47
+ setAIProvider(mockAIProvider)
48
+ // aiExport is free (grid) and delegates the WRITE to the registered engine;
49
+ // register enterprise's exportGrid so the download path actually runs.
50
+ registerExportProvider(exportGrid as never)
51
+ })
52
+ afterEach(() => setAIProvider(null))
53
+
54
+ describe('aiExport', () => {
55
+ it('parses format + groupBy + filters; applies to the grid only when asked', async () => {
56
+ const api = makeApi()
57
+ const plan = await aiExport(api, 'export deals over 500 grouped by region as a pdf', {
58
+ run: false,
59
+ apply: true,
60
+ })
61
+ expect(plan.format).toBe('pdf')
62
+ expect(plan.groupBy).toEqual(['region'])
63
+ expect(plan.filters).toContainEqual({ field: 'amount', operator: 'greaterThan', value: '500' })
64
+ // apply: true -> the grid is mutated
65
+ expect(api.clearAllFilters).toHaveBeenCalled()
66
+ expect(api.setFilter).toHaveBeenCalledWith('amount', { operator: 'greaterThan', value: '500' })
67
+ // run:false -> nothing downloaded
68
+ expect(downloads).toHaveLength(0)
69
+ })
70
+
71
+ it('defaults to xlsx and does NOT touch the grid (apply defaults off)', async () => {
72
+ const api = makeApi()
73
+ const plan = await aiExport(api, 'just export everything', { run: false })
74
+ expect(plan.format).toBe('xlsx')
75
+ expect(api.clearAllFilters).not.toHaveBeenCalled()
76
+ expect(api.setFilter).not.toHaveBeenCalled()
77
+ })
78
+
79
+ it('runs the export (downloads) when run is not false', async () => {
80
+ const api = makeApi()
81
+ await aiExport(api, 'export as csv', { filename: 'report' })
82
+ expect(downloads).toHaveLength(1)
83
+ expect(downloads[0]!.filename).toBe('report.csv')
84
+ })
85
+
86
+ it('exports the plan-filtered rows directly (not the grid displayed rows)', async () => {
87
+ // getDisplayedRows returns EVERYTHING (as if the grid hadn't re-filtered);
88
+ // the export must still contain only rows matching the plan's filter.
89
+ const api = makeApi()
90
+ await aiExport(api, 'export deals over 500 as csv', { filename: 'big' })
91
+ const { text } = downloads[0]!
92
+ expect(text).toContain('2000') // amount 2000 > 500 kept
93
+ expect(text).not.toContain('100') // amount 100 filtered out
94
+ expect(text).not.toContain('50') // amount 50 filtered out
95
+ })
96
+ })
97
+
98
+ describe('aiFindAnomalies', () => {
99
+ it('flags a numeric outlier over the scanned rows', async () => {
100
+ const api = makeApi()
101
+ const result = await aiFindAnomalies(api, {})
102
+ expect(result.anomalies.length).toBeGreaterThan(0)
103
+ const amountAnomaly = result.anomalies.find((a) => a.field === 'amount')
104
+ expect(amountAnomaly?.value).toBe(2000) // the max in the sample
105
+ expect(amountAnomaly?.severity).toBe('medium')
106
+ expect(result.summary).toBeTruthy()
107
+ })
108
+
109
+ it('returns empty for an empty selection', async () => {
110
+ const api = makeApi()
111
+ const result = await aiFindAnomalies(api, { target: { kind: 'selection', rowIndices: [] } })
112
+ expect(result.anomalies).toEqual([])
113
+ })
114
+ })
@@ -83,6 +83,10 @@ const XML_ESCAPE: Record<string, string> = {
83
83
  "'": '&apos;',
84
84
  }
85
85
  // Control chars that are illegal in XML 1.0 (would make Excel reject the file).
86
+ // XML 1.0 forbids these control characters outright, so they are stripped
87
+ // rather than escaped - a stray \x00 from a data source would otherwise
88
+ // produce a workbook Excel refuses to open.
89
+ // eslint-disable-next-line no-control-regex
86
90
  const INVALID_XML = /[\x00-\x08\x0B\x0C\x0E-\x1F]/g
87
91
  function esc(s: string): string {
88
92
  return s.replace(INVALID_XML, '').replace(/[&<>"']/g, (c) => XML_ESCAPE[c]!)
package/src/export-xls.ts CHANGED
@@ -17,6 +17,9 @@ import type { RowData } from '@svgrid/grid'
17
17
  import type { SerializeOptions } from './export-serialize'
18
18
 
19
19
  // Control chars illegal in XML 1.0 - strip so a stray \x00 can't corrupt the file.
20
+ // See export-ooxml.ts: XML 1.0 has no representation for these at all, so a
21
+ // control character has to be dropped before it reaches the file.
22
+ // eslint-disable-next-line no-control-regex
20
23
  const INVALID_XML = /[\x00-\x08\x0B\x0C\x0E-\x1F]/g
21
24
  function xmlEscape(s: string): string {
22
25
  return s
@@ -103,6 +103,11 @@ export function evaluateScalar<TData = ExprRow>(
103
103
  return b === 0 ? NaN : a / b
104
104
  case '%':
105
105
  return b === 0 ? NaN : a % b
106
+ // Rules are authored at runtime, so an operator outside the union can
107
+ // arrive from parsed JSON. Without this the switch falls out of its
108
+ // case and into `case 'agg'`, reading agg fields off a binary node.
109
+ default:
110
+ return null
106
111
  }
107
112
  }
108
113
  case 'agg': {
@@ -123,6 +128,10 @@ export function evaluateScalar<TData = ExprRow>(
123
128
  return Math.min(...nums)
124
129
  case 'max':
125
130
  return Math.max(...nums)
131
+ // Same here: falling through to `case 'func'` would call
132
+ // `expr.name.toUpperCase()` on an agg node and throw.
133
+ default:
134
+ return null
126
135
  }
127
136
  }
128
137
  case 'func': {
@@ -250,7 +250,7 @@ describe('validation + commit', () => {
250
250
  const { api } = fakeApi()
251
251
  const r = await importData(api, {
252
252
  file: csv, format: 'csv',
253
- validator: (row, i) => {
253
+ validator: (row, _i) => {
254
254
  const errs = []
255
255
  const price = (row as any).price as number
256
256
  if (price < 0) errs.push({ field: 'price', message: 'must be >= 0' })
package/src/import.ts CHANGED
@@ -658,7 +658,7 @@ function coerceTyped(raw: string, type: ImportFieldType):
658
658
  if (iso) return { ok: true, value: `${iso[1]}-${iso[2]}-${iso[3]}` }
659
659
  // mm/dd/yyyy or dd/mm/yyyy - we don't try to disambiguate; the
660
660
  // demo can declare the locale via its own validator if it cares.
661
- const slash = trimmed.match(/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/)
661
+ const slash = trimmed.match(/^(\d{1,2})[/-](\d{1,2})[/-](\d{4})/)
662
662
  if (slash) {
663
663
  const [, a, b, y] = slash
664
664
  // Default to mm/dd/yyyy because it's the dominant locale where
package/src/index.ts CHANGED
@@ -201,6 +201,7 @@ export {
201
201
  type PlanOp,
202
202
  type KitDataSourceOptions,
203
203
  type KitHandlerOptions,
204
+ type KitScope,
204
205
  type KitHandlers,
205
206
  type KitMessage,
206
207
  type SqlDialect,
@@ -321,6 +322,17 @@ export {
321
322
  liveDataSamples,
322
323
  getLiveDataSample,
323
324
  type SampleApp,
325
+ starterDatasets,
326
+ getStarterDataset,
327
+ type StarterDataset,
328
+ crudSuiteScreens,
329
+ addCrudSuite,
330
+ crudAppFromSchemas,
331
+ type CrudScreenKind,
332
+ type CrudEditingMode,
333
+ type CrudSuiteOptions,
334
+ type CrudAppOptions,
335
+ introspectJson,
324
336
  sanitizeStudioProject,
325
337
  buildStudioBugReport,
326
338
  type BugReport,
package/src/pivot.test.ts CHANGED
@@ -8,7 +8,6 @@ import {
8
8
  filterCollapsedPivotRows,
9
9
  pivotAggregators,
10
10
  type PivotConfig,
11
- type PivotRow,
12
11
  } from './pivot'
13
12
 
14
13
  type Sale = {
@@ -60,7 +60,7 @@ export function updateField(
60
60
  if (f.field !== fieldName) {
61
61
  return makesPrimary && f.primaryKey ? { ...f, primaryKey: false } : f
62
62
  }
63
- let next: EntityField = { ...f, ...patch }
63
+ const next: EntityField = { ...f, ...patch }
64
64
  if (patch.type && patch.type !== f.type) {
65
65
  if (patch.type !== 'enum') delete next.options
66
66
  if (patch.type !== 'relation') delete next.relation
@@ -11,8 +11,8 @@
11
11
  * whose OpenAPI is blocked) - the caller can then ask the user to list columns.
12
12
  */
13
13
  import type { RowData } from '@svgrid/grid'
14
- import type { EntityFieldType, EntitySchema } from '../schema'
15
- import { buildEntitySchema, type IntrospectedColumn } from './schema-from-columns'
14
+ import type { EntityFieldType, EntitySchema } from '../schema.js'
15
+ import { buildEntitySchema, type IntrospectedColumn } from './schema-from-columns.js'
16
16
 
17
17
  export type IntrospectSupabaseOptions = {
18
18
  /** Project URL, e.g. `https://xxxx.supabase.co`. */