@svgrid/enterprise 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/dist/cdn/svgrid-enterprise.svelte-external.js +26039 -805
  2. package/dist/designer/assets/index-Dp44bTid.js +939 -0
  3. package/dist/designer/assets/index-RJp6x8tw.css +1 -0
  4. package/dist/designer/assets/jszip.min-CjMo-QGg.js +2 -0
  5. package/dist/designer/index.html +13 -0
  6. package/dist/node/studio.js +22554 -0
  7. package/package.json +10 -3
  8. package/src/SvAuthGate.svelte +115 -0
  9. package/src/SvBoard.dom.test.ts +67 -0
  10. package/src/SvBoard.svelte +192 -0
  11. package/src/SvExportMenu.svelte +553 -0
  12. package/src/SvFileInput.svelte +113 -0
  13. package/src/SvGridEditPanel.dom.test.ts +146 -0
  14. package/src/SvGridEditPanel.svelte +793 -0
  15. package/src/SvGridMasterDetail.svelte +95 -0
  16. package/src/SvImportDialog.svelte +1020 -0
  17. package/src/SvLookupInput.svelte +180 -0
  18. package/src/SvRecordDetail.dom.test.ts +137 -0
  19. package/src/SvRecordDetail.svelte +288 -0
  20. package/src/SvSchedule.dom.test.ts +57 -0
  21. package/src/SvSchedule.svelte +156 -0
  22. package/src/SvSchemaChart.svelte +233 -0
  23. package/src/SvSchemaDashboard.svelte +130 -0
  24. package/src/ai-export-pdf.test.ts +74 -0
  25. package/src/ai-export-xlsx.test.ts +87 -0
  26. package/src/ai-export.test.ts +110 -0
  27. package/src/ai.ts +1188 -782
  28. package/src/edit-panel.test.ts +213 -0
  29. package/src/edit-panel.ts +228 -0
  30. package/src/export-conditional.test.ts +60 -0
  31. package/src/export-conditional.ts +94 -0
  32. package/src/export-ooxml.test.ts +152 -0
  33. package/src/export-ooxml.ts +485 -0
  34. package/src/export-pdf.test.ts +138 -0
  35. package/src/export-pdf.ts +245 -0
  36. package/src/export-print.test.ts +66 -0
  37. package/src/export-print.ts +132 -0
  38. package/src/export-serialize.test.ts +56 -0
  39. package/src/export-serialize.ts +24 -0
  40. package/src/export-xls.ts +196 -0
  41. package/src/export-xlsx-roundtrip.test.ts +120 -0
  42. package/src/export-xlsx.test.ts +150 -0
  43. package/src/export.test.ts +349 -0
  44. package/src/export.ts +1452 -549
  45. package/src/import-automap.test.ts +131 -0
  46. package/src/import-hardening.test.ts +158 -0
  47. package/src/import.ts +1042 -648
  48. package/src/index.ts +282 -0
  49. package/src/install.ts +134 -114
  50. package/src/license-core.test.ts +23 -0
  51. package/src/license-core.ts +35 -0
  52. package/src/license.ts +103 -90
  53. package/src/master-detail.test.ts +61 -0
  54. package/src/master-detail.ts +42 -0
  55. package/src/print.ts +107 -127
  56. package/src/schema-designer.test.ts +121 -0
  57. package/src/schema-designer.ts +142 -0
  58. package/src/schema.test.ts +268 -0
  59. package/src/schema.ts +499 -0
  60. package/src/smart-shim.ts +107 -105
  61. package/src/sources/aggregate.test.ts +79 -0
  62. package/src/sources/aggregate.ts +102 -0
  63. package/src/sources/auth-supabase.test.ts +80 -0
  64. package/src/sources/auth-supabase.ts +87 -0
  65. package/src/sources/dashboard.kpi.test.ts +50 -0
  66. package/src/sources/dashboard.test.ts +61 -0
  67. package/src/sources/dashboard.ts +136 -0
  68. package/src/sources/field-inference.test.ts +60 -0
  69. package/src/sources/field-inference.ts +63 -0
  70. package/src/sources/filters.test.ts +58 -0
  71. package/src/sources/filters.ts +64 -0
  72. package/src/sources/index.ts +67 -0
  73. package/src/sources/introspect-supabase.test.ts +100 -0
  74. package/src/sources/introspect-supabase.ts +119 -0
  75. package/src/sources/realtime-supabase.test.ts +93 -0
  76. package/src/sources/realtime-supabase.ts +99 -0
  77. package/src/sources/relation-lookup.test.ts +99 -0
  78. package/src/sources/relation-lookup.ts +128 -0
  79. package/src/sources/rest-adapters.test.ts +95 -0
  80. package/src/sources/rest-adapters.ts +120 -0
  81. package/src/sources/rest.test.ts +104 -0
  82. package/src/sources/rest.ts +129 -0
  83. package/src/sources/schema-from-columns.test.ts +106 -0
  84. package/src/sources/schema-from-columns.ts +88 -0
  85. package/src/sources/supabase.test.ts +110 -0
  86. package/src/sources/supabase.ts +99 -0
  87. package/src/sources/with-entity-rules.test.ts +104 -0
  88. package/src/sources/with-entity-rules.ts +78 -0
  89. package/src/sources/with-relation-labels.test.ts +75 -0
  90. package/src/sources/with-relation-labels.ts +73 -0
  91. package/src/studio/bug-report.test.ts +101 -0
  92. package/src/studio/bug-report.ts +165 -0
  93. package/src/studio/cli.test.ts +187 -0
  94. package/src/studio/cli.ts +112 -0
  95. package/src/studio/csv.test.ts +90 -0
  96. package/src/studio/csv.ts +165 -0
  97. package/src/studio/db-connect-string.test.ts +94 -0
  98. package/src/studio/db-connect-string.ts +128 -0
  99. package/src/studio/emit-project.test.ts +923 -0
  100. package/src/studio/emit-project.ts +1273 -0
  101. package/src/studio/emit-schema.test.ts +94 -0
  102. package/src/studio/emit-schema.ts +920 -0
  103. package/src/studio/index.ts +195 -0
  104. package/src/studio/introspect-db.test.ts +186 -0
  105. package/src/studio/introspect-db.ts +312 -0
  106. package/src/studio/introspect-prisma.test.ts +99 -0
  107. package/src/studio/introspect-prisma.ts +175 -0
  108. package/src/studio/introspect.test.ts +172 -0
  109. package/src/studio/introspect.ts +310 -0
  110. package/src/studio/pipeline.test.ts +42 -0
  111. package/src/studio/project-robust.test.ts +157 -0
  112. package/src/studio/project.test.ts +473 -0
  113. package/src/studio/project.ts +916 -0
  114. package/src/studio/sample-data.test.ts +58 -0
  115. package/src/studio/sample-data.ts +200 -0
  116. package/src/studio/samples/ats.ts +202 -0
  117. package/src/studio/samples/clinic.ts +177 -0
  118. package/src/studio/samples/crm.ts +250 -0
  119. package/src/studio/samples/ecommerce.ts +187 -0
  120. package/src/studio/samples/events.ts +199 -0
  121. package/src/studio/samples/fleet.ts +184 -0
  122. package/src/studio/samples/gym.ts +213 -0
  123. package/src/studio/samples/hr.ts +193 -0
  124. package/src/studio/samples/index.ts +55 -0
  125. package/src/studio/samples/insurance.ts +195 -0
  126. package/src/studio/samples/inventory.ts +182 -0
  127. package/src/studio/samples/invoicing.ts +162 -0
  128. package/src/studio/samples/library.ts +180 -0
  129. package/src/studio/samples/live-data.test.ts +98 -0
  130. package/src/studio/samples/live-data.ts +308 -0
  131. package/src/studio/samples/projects.ts +190 -0
  132. package/src/studio/samples/realestate.ts +196 -0
  133. package/src/studio/samples/restaurant.ts +187 -0
  134. package/src/studio/samples/samples.test.ts +208 -0
  135. package/src/studio/samples/school.ts +197 -0
  136. package/src/studio/samples/seed-floor.test.ts +25 -0
  137. package/src/studio/samples/shared.ts +305 -0
  138. package/src/studio/samples/subscriptions.ts +183 -0
  139. package/src/studio/samples/support.ts +182 -0
  140. package/src/studio/scaffold-app.test.ts +91 -0
  141. package/src/studio/scaffold-app.ts +161 -0
  142. package/src/studio/scaffold.test.ts +178 -0
  143. package/src/studio/scaffold.ts +374 -0
  144. package/src/studio/themes.ts +172 -0
  145. package/src/studio/verify.test.ts +47 -0
  146. package/src/studio/verify.ts +79 -0
  147. package/src/sveltekit/in-memory.test.ts +104 -0
  148. package/src/sveltekit/in-memory.ts +129 -0
  149. package/src/sveltekit/index.ts +23 -0
  150. package/src/sveltekit/query-plan.test.ts +109 -0
  151. package/src/sveltekit/query-plan.ts +125 -0
  152. package/src/sveltekit/sql-source.test.ts +185 -0
  153. package/src/sveltekit/sql-source.ts +166 -0
  154. package/src/sveltekit/sql.test.ts +80 -0
  155. package/src/sveltekit/sql.ts +131 -0
  156. package/src/sveltekit/transport.test.ts +175 -0
  157. package/src/sveltekit/transport.ts +254 -0
  158. package/src/sveltekit/types.ts +11 -0
  159. package/src/upgrade-prompt.ts +149 -148
@@ -0,0 +1,131 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
2
+ import {
3
+ importData,
4
+ readImportMatrix,
5
+ mapImportMatrix,
6
+ autoMapColumns,
7
+ inferImportColumnTypes,
8
+ type ImportGridColumn,
9
+ } from './import'
10
+ import { clearLicenseKey, setLicenseKey } from './license'
11
+
12
+ // The grid columns the auto-mapper matches source headers against. Headers
13
+ // are phrased DIFFERENTLY from the file on purpose (Unit Price / Sold On) so
14
+ // the fuzzy match + type inference are actually exercised.
15
+ const gridColumns: ImportGridColumn[] = [
16
+ { field: 'company', header: 'Company' },
17
+ { field: 'country', header: 'Country' },
18
+ { field: 'sellDate', header: 'Sell date', format: { type: 'date' } },
19
+ { field: 'quantity', header: 'Qty', format: { type: 'number' } },
20
+ { field: 'price', header: 'Unit price', format: { type: 'currency' } },
21
+ ]
22
+
23
+ function fakeApi() {
24
+ const calls: Array<{ method: string; args: unknown[] }> = []
25
+ const api = {
26
+ getColumns: () => gridColumns,
27
+ addRows: (...args: unknown[]) => calls.push({ method: 'addRows', args }),
28
+ }
29
+ return { api: api as any, calls }
30
+ }
31
+
32
+ beforeEach(() => setLicenseKey('SVENTERPRISE-DEV-TEST'))
33
+ afterEach(() => clearLicenseKey())
34
+
35
+ describe('autoMapColumns', () => {
36
+ it('matches headers to fields by label or field name, ignoring case + spacing', () => {
37
+ const map = autoMapColumns(
38
+ ['Company', 'unit_price', 'SellDate', 'Unknown Col'],
39
+ gridColumns,
40
+ )
41
+ expect(map['Company']).toBe('company')
42
+ expect(map['unit_price']).toBe('price') // "unit_price" ~ "Unit price"
43
+ expect(map['SellDate']).toBe('sellDate') // field-name match
44
+ expect('Unknown Col' in map).toBe(false) // no confident match -> left out
45
+ })
46
+ })
47
+
48
+ describe('inferImportColumnTypes', () => {
49
+ it('derives strict types from each column format', () => {
50
+ const types = inferImportColumnTypes(gridColumns)
51
+ expect(types).toEqual({
52
+ sellDate: 'date',
53
+ quantity: 'number',
54
+ price: 'number', // currency -> number
55
+ })
56
+ })
57
+ })
58
+
59
+ describe('mapImportMatrix', () => {
60
+ it('applies a column map + types to a pre-parsed matrix (no I/O)', () => {
61
+ const matrix = [
62
+ ['Company', 'Unit Price'],
63
+ ['ACME', '$1,299.00'],
64
+ ['', ''], // blank row -> skipped
65
+ ]
66
+ const { rows, skipped, total, errors } = mapImportMatrix(matrix, {
67
+ columnMap: { 'Company': 'company', 'Unit Price': 'price' },
68
+ columnTypes: { price: 'number' },
69
+ })
70
+ expect(errors).toHaveLength(0)
71
+ expect(rows).toEqual([{ company: 'ACME', price: 1299 }])
72
+ expect(skipped).toBe(1)
73
+ expect(total).toBe(2)
74
+ })
75
+
76
+ it('reports a coercion error but parks the raw value', () => {
77
+ const { rows, errors } = mapImportMatrix(
78
+ [['Price'], ['not-a-price']],
79
+ { columnMap: { Price: 'price' }, columnTypes: { price: 'number' } },
80
+ )
81
+ expect(errors).toEqual([{ rowIndex: 0, field: 'price', message: expect.stringContaining('not a number') }])
82
+ expect((rows[0] as Record<string, unknown>).price).toBe('not-a-price')
83
+ })
84
+ })
85
+
86
+ describe('readImportMatrix', () => {
87
+ it('parses inline CSV into a header-first matrix', async () => {
88
+ const { format, matrix } = await readImportMatrix('a,b\n1,2', 'auto')
89
+ expect(format).toBe('csv')
90
+ expect(matrix).toEqual([['a', 'b'], ['1', '2']])
91
+ })
92
+
93
+ it('sniffs JSON text and flattens objects to a header-first matrix', async () => {
94
+ const { format, matrix } = await readImportMatrix('[{"x":1},{"x":2}]')
95
+ expect(format).toBe('json')
96
+ expect(matrix).toEqual([['x'], ['1'], ['2']])
97
+ })
98
+ })
99
+
100
+ describe('importData autoMap', () => {
101
+ it('lines a differently-headed file up with the grid and coerces by format', async () => {
102
+ const { api } = fakeApi()
103
+ // Headers phrased differently in case/spacing from the grid labels
104
+ // ("Sell Date" ~ "Sell date", "unit price" ~ "Unit price") still line up.
105
+ const csv = [
106
+ 'Company,Country,Sell Date,Qty,unit price',
107
+ 'Initech,USA,2026-05-02,12,"$1,299.00"',
108
+ ].join('\n')
109
+ const r = await importData(api, { file: csv, format: 'csv', autoMap: true })
110
+ expect(r.errors).toHaveLength(0)
111
+ expect(r.rows[0]).toEqual({
112
+ company: 'Initech',
113
+ country: 'USA',
114
+ sellDate: '2026-05-02',
115
+ quantity: 12,
116
+ price: 1299, // "$1,299.00" -> number via the currency column
117
+ })
118
+ })
119
+
120
+ it('lets an explicit columnMap override the guess', async () => {
121
+ const { api } = fakeApi()
122
+ const r = await importData(api, {
123
+ file: 'Company,Widget Cost\nACME,$5.00',
124
+ format: 'csv',
125
+ autoMap: true,
126
+ columnMap: { 'Widget Cost': 'price' },
127
+ columnTypes: { price: 'number' },
128
+ })
129
+ expect(r.rows[0]).toEqual({ company: 'ACME', price: 5 })
130
+ })
131
+ })
@@ -0,0 +1,158 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
2
+ import { importData, mapImportMatrix, mapImportMatrixAsync, readImportMatrix } from './import'
3
+ import { clearLicenseKey, setLicenseKey } from './license'
4
+
5
+ function fakeApi() {
6
+ const calls: Array<{ method: string; args: unknown[] }> = []
7
+ const api = {
8
+ getColumns: () => [{ field: 'name', header: 'Name' }],
9
+ addRows: (...args: unknown[]) => calls.push({ method: 'addRows', args }),
10
+ }
11
+ return { api: api as any, calls }
12
+ }
13
+
14
+ beforeEach(() => setLicenseKey('SVENTERPRISE-DEV-TEST'))
15
+ afterEach(() => clearLicenseKey())
16
+
17
+ describe('security: prototype pollution', () => {
18
+ // NOTE: each test here must FAIL if the UNSAFE_FIELDS guard is removed.
19
+ // A string assigned to `__proto__` is a JS no-op, so a naive test would
20
+ // pass either way - we force real reparenting with an object value, or use
21
+ // `constructor` (a normal writable key that DOES become an own prop).
22
+
23
+ it('drops a "constructor" header instead of writing it as an own prop', async () => {
24
+ const { api } = fakeApi()
25
+ // Without the guard: rec.constructor = 'X' becomes a real own property.
26
+ const r = await importData(api, { file: 'constructor,name\nX,ok', format: 'csv' })
27
+ expect(Object.prototype.hasOwnProperty.call(r.rows[0], 'constructor')).toBe(false)
28
+ expect(r.rows[0]).toEqual({ name: 'ok' })
29
+ })
30
+
31
+ it('never reparents a row through a __proto__ column, even with json coercion', async () => {
32
+ const { api } = fakeApi()
33
+ // With json coercion the cell becomes an OBJECT; without the guard,
34
+ // `rec['__proto__'] = { isAdmin: true }` would reparent the row so that
35
+ // `row.isAdmin` reads true off the polluted prototype.
36
+ const json = '[{"__proto__":{"isAdmin":true},"id":1}]'
37
+ const r = await importData(api, { file: json, format: 'json', columnTypes: { __proto__: 'json' } })
38
+ expect(Object.getPrototypeOf(r.rows[0])).toBe(Object.prototype)
39
+ expect((r.rows[0] as Record<string, unknown>).isAdmin).toBeUndefined()
40
+ expect(r.rows[0]).toEqual({ id: 1 })
41
+ })
42
+
43
+ it('drops constructor / prototype JSON keys', async () => {
44
+ const { api } = fakeApi()
45
+ // Both are own enumerable keys on the parsed object, so without the guard
46
+ // they land in the row and break the toEqual.
47
+ const json = JSON.stringify([{ constructor: 'x', prototype: 'y', a: 1 }])
48
+ const r = await importData(api, { file: json, format: 'json' })
49
+ expect(r.rows[0]).toEqual({ a: 1 })
50
+ })
51
+
52
+ it('refuses an unsafe target even from an explicit columnMap', () => {
53
+ // Exercises the columnMap branch of computeFields (not the default one).
54
+ const r = mapImportMatrix(
55
+ [['h', 'name'], ['{"x":1}', 'ok']],
56
+ { columnMap: { h: '__proto__' }, columnTypes: { __proto__: 'json' } },
57
+ )
58
+ expect(Object.getPrototypeOf(r.rows[0])).toBe(Object.prototype)
59
+ expect(r.rows[0]).toEqual({ name: 'ok' })
60
+ })
61
+
62
+ it('skips a blank header rather than writing an empty-key field', () => {
63
+ // Without the guard, the empty header maps to field '' and rec[''] = 'orphan'.
64
+ const r = mapImportMatrix([['name', ''], ['A', 'orphan']])
65
+ expect(r.rows[0]).toEqual({ name: 'A' })
66
+ })
67
+ })
68
+
69
+ describe('guard-rails: size + rows + errors', () => {
70
+ it('rejects a File/Blob over maxBytes before reading', async () => {
71
+ const { api } = fakeApi()
72
+ const blob = new Blob(['a,b\n1,2\n'])
73
+ await expect(importData(api, { file: blob, format: 'csv', maxBytes: 3 })).rejects.toThrow(/import limit/)
74
+ })
75
+
76
+ it('rejects more rows than maxRows', async () => {
77
+ const { api } = fakeApi()
78
+ const csv = 'name\n' + Array.from({ length: 5 }, (_, i) => `r${i}`).join('\n')
79
+ await expect(importData(api, { file: csv, format: 'csv', maxRows: 3 })).rejects.toThrow(/maxRows/)
80
+ })
81
+
82
+ it("overLimit:'truncate' keeps the first maxRows and flags it", async () => {
83
+ const { api } = fakeApi()
84
+ const csv = 'name\n' + Array.from({ length: 5 }, (_, i) => `r${i}`).join('\n')
85
+ const r = await importData(api, { file: csv, format: 'csv', maxRows: 3, overLimit: 'truncate' })
86
+ expect(r.rows).toHaveLength(3)
87
+ expect(r.rows.map((x: any) => x.name)).toEqual(['r0', 'r1', 'r2'])
88
+ expect(r.truncated).toBe(true)
89
+ expect(r.total).toBe(5) // original count preserved
90
+ })
91
+
92
+ it('caps collected errors at maxErrors and flags truncation', async () => {
93
+ const { api } = fakeApi()
94
+ const csv = 'n\nx\ny\nz\nw'
95
+ const r = await importData(api, {
96
+ file: csv,
97
+ format: 'csv',
98
+ columnTypes: { n: 'number' },
99
+ maxErrors: 2,
100
+ })
101
+ expect(r.errors).toHaveLength(2)
102
+ expect(r.errorsTruncated).toBe(true)
103
+ })
104
+ })
105
+
106
+ describe('dedupeBy', () => {
107
+ it('keeps the last occurrence of each key, in place', () => {
108
+ const r = mapImportMatrix(
109
+ [
110
+ ['id', 'v'],
111
+ ['1', 'a'],
112
+ ['2', 'b'],
113
+ ['1', 'c'], // dup of id 1 -> overrides "a" at position 0
114
+ ],
115
+ { dedupeBy: 'id' },
116
+ )
117
+ expect(r.rows).toEqual([{ id: 1, v: 'c' }, { id: 2, v: 'b' }])
118
+ expect(r.deduped).toBe(1)
119
+ })
120
+ })
121
+
122
+ describe('cancellation', () => {
123
+ it('throws AbortError when the signal is already aborted', async () => {
124
+ const { api } = fakeApi()
125
+ const controller = new AbortController()
126
+ controller.abort()
127
+ await expect(
128
+ importData(api, { file: 'name\nA', format: 'csv', signal: controller.signal }),
129
+ ).rejects.toMatchObject({ name: 'AbortError' })
130
+ })
131
+ })
132
+
133
+ describe('mapImportMatrixAsync', () => {
134
+ it('matches the sync mapper and reports progress to completion', async () => {
135
+ const matrix = [['name'], ...Array.from({ length: 10 }, (_, i) => [`n${i}`])]
136
+ const ticks: number[] = []
137
+ const r = await mapImportMatrixAsync(matrix, {
138
+ chunkSize: 3,
139
+ onProgress: (p) => ticks.push(p.ratio),
140
+ })
141
+ expect(r.rows).toHaveLength(10)
142
+ expect(r.rows).toEqual(mapImportMatrix(matrix).rows)
143
+ expect(ticks.at(-1)).toBe(1) // finishes at 100%
144
+ })
145
+ })
146
+
147
+ describe('encoding', () => {
148
+ it('decodes a non-UTF-8 (windows-1252) CSV blob', async () => {
149
+ // 0xE9 is "é" in windows-1252 but an invalid lone byte in UTF-8.
150
+ const bytes = new Uint8Array([
151
+ ...'name\nCaf'.split('').map((c) => c.charCodeAt(0)),
152
+ 0xe9, // é
153
+ ])
154
+ const blob = new Blob([bytes])
155
+ const { matrix } = await readImportMatrix(blob, 'csv', { encoding: 'windows-1252' })
156
+ expect(matrix[1]).toEqual(['Café'])
157
+ })
158
+ })