@svgrid/enterprise 2.2.1 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -81
- package/dist/cdn/svgrid-enterprise.svelte-external.js +26642 -11252
- package/dist/designer/assets/GridMenus-BtWVk9Ab.js +7 -0
- package/dist/designer/assets/SvGridChartPanel-DWlBO2SD.js +10 -0
- package/dist/designer/assets/SvGridChartView-8c8Mb4j8.js +1 -0
- package/dist/designer/assets/SvGridChartView-DX9HfkBR.css +1 -0
- package/dist/designer/assets/index-DsDgp9Xq.js +78758 -0
- package/dist/designer/assets/index-tTY_Dx4P.css +1 -0
- package/dist/designer/assets/jszip.min-fkJdmAmj.js +2 -0
- package/dist/designer/assets/pdfmake-DeCsnyl9.js +242 -0
- package/dist/designer/assets/smart.export-BZlSCE8T.js +35 -0
- package/dist/designer/assets/vfs_fonts-eX2NpmfX.js +1 -0
- package/dist/designer/index.html +13 -0
- package/dist/node/studio.js +15304 -2708
- package/package.json +35 -11
- package/src/SvAlertRuleEditor.svelte +294 -0
- package/src/SvAlertsManager.svelte +210 -0
- package/src/SvAlertsPanel.svelte +129 -0
- package/src/SvBoard.svelte +3 -1
- package/src/SvExpressionEditor.svelte +341 -0
- package/src/SvGridAlerts.dom.test.ts +113 -0
- package/src/SvGridAlerts.svelte +265 -0
- package/src/SvGridBoard.svelte +2358 -0
- package/src/SvGridEditPanel.svelte +849 -794
- package/src/SvGridScheduler.svelte +5358 -4410
- package/src/SvPivotDesigner.svelte +3 -3
- package/src/SvRecordDetail.svelte +6 -2
- package/src/SvSchedule.svelte +3 -1
- package/src/{ai-export-pdf.test.ts → ai-export-pdf.dom.test.ts} +77 -74
- package/src/{ai-export-xlsx.test.ts → ai-export-xlsx.dom.test.ts} +90 -87
- package/src/{ai-export.test.ts → ai-export.dom.test.ts} +114 -110
- package/src/alerts/alert-engine-attach.ts +165 -0
- package/src/alerts/alert-engine.test.ts +135 -0
- package/src/alerts/alert-engine.ts +260 -0
- package/src/alerts/alert-formats.test.ts +87 -0
- package/src/alerts/alert-formats.ts +77 -0
- package/src/alerts/alert-observer.test.ts +189 -0
- package/src/alerts/alert-observer.ts +208 -0
- package/src/alerts/alert-scheduler.ts +96 -0
- package/src/alerts/alert-storage.test.ts +54 -0
- package/src/alerts/alert-storage.ts +116 -0
- package/src/alerts/alert-store.svelte.ts +80 -0
- package/src/alerts/alert-types.ts +94 -0
- package/src/alerts.ts +28 -0
- package/src/board.dom.test.ts +941 -0
- package/src/board.ts +36 -0
- package/src/export-ooxml.ts +4 -0
- package/src/export-xls.ts +3 -0
- package/src/export.ts +6 -0
- package/src/expressions/evaluate.test.ts +111 -0
- package/src/expressions/evaluate.ts +237 -0
- package/src/expressions/expression-columns.ts +133 -0
- package/src/expressions/expression-types.ts +84 -0
- package/src/expressions/parse.test.ts +106 -0
- package/src/expressions/parse.ts +610 -0
- package/src/import.test.ts +1 -1
- package/src/import.ts +3 -1
- package/src/index.ts +192 -46
- package/src/install.ts +12 -2
- package/src/pivot-enable.ts +44 -0
- package/src/pivot.test.ts +0 -1
- package/src/scheduler-assignments.test.ts +97 -0
- package/src/scheduler-assignments.ts +134 -0
- package/src/scheduler-axis.test.ts +108 -0
- package/src/scheduler-axis.ts +238 -0
- package/src/scheduler-booking.test.ts +57 -0
- package/src/scheduler-booking.ts +63 -0
- package/src/scheduler-config.ts +179 -0
- package/src/scheduler-dependencies.test.ts +155 -0
- package/src/scheduler-dependencies.ts +223 -0
- package/src/scheduler-freebusy.test.ts +41 -0
- package/src/scheduler-freebusy.ts +36 -0
- package/src/scheduler-heatmap.test.ts +39 -0
- package/src/scheduler-heatmap.ts +55 -0
- package/src/scheduler-resource-tree.test.ts +84 -0
- package/src/scheduler-resource-tree.ts +107 -0
- package/src/scheduler-slots.test.ts +53 -0
- package/src/scheduler-slots.ts +94 -0
- package/src/scheduler-summary.test.ts +47 -0
- package/src/scheduler-summary.ts +81 -0
- package/src/schema-designer.ts +1 -1
- package/src/sources/index.ts +1 -1
- package/src/sources/introspect-supabase.test.ts +13 -1
- package/src/sources/introspect-supabase.ts +20 -0
- package/src/studio/copilot-core.test.ts +45 -0
- package/src/studio/copilot-core.ts +65 -0
- package/src/studio/deploy-cli.test.ts +56 -0
- package/src/studio/deploy-cli.ts +100 -0
- package/src/studio/emit-project.test.ts +800 -24
- package/src/studio/emit-project.ts +1307 -135
- package/src/studio/emit-schema.test.ts +17 -0
- package/src/studio/emit-schema.ts +182 -41
- package/src/studio/index.ts +59 -1
- package/src/studio/init-flow.test.ts +239 -0
- package/src/studio/init-flow.ts +358 -0
- package/src/studio/introspect-openapi.test.ts +84 -0
- package/src/studio/introspect-openapi.ts +252 -0
- package/src/studio/project.test.ts +57 -1
- package/src/studio/project.ts +302 -8
- package/src/studio/samples/crm.ts +282 -258
- package/src/studio/samples/datasets.test.ts +84 -0
- package/src/studio/samples/datasets.ts +340 -0
- package/src/studio/samples/fleet.ts +213 -186
- package/src/studio/samples/insurance.ts +223 -195
- package/src/studio/samples/inventory.ts +213 -182
- package/src/studio/samples/live-data.test.ts +99 -98
- package/src/studio/samples/live-data.ts +8 -10
- package/src/studio/samples/projects.ts +212 -190
- package/src/studio/samples/samples.test.ts +268 -216
- package/src/studio/samples/shared.ts +22 -145
- package/src/studio/samples/starter.ts +251 -0
- package/src/studio/samples/support.ts +209 -184
- package/src/studio/screen-suites.test.ts +226 -0
- package/src/studio/screen-suites.ts +445 -0
- package/src/studio/ui-components-surface.test.ts +185 -0
- package/src/studio/ui-components.generated.ts +7154 -0
- package/src/studio/ui-components.ts +273 -3
- package/src/sveltekit/index.ts +1 -0
- package/src/sveltekit/sql-source.test.ts +13 -0
- package/src/sveltekit/sql-source.ts +12 -7
- package/src/sveltekit/transport-scope.test.ts +125 -0
- package/src/sveltekit/transport.ts +76 -2
- package/src/upgrade-prompt.ts +2 -2
- package/src/watermark.ts +2 -2
- package/src/ai.test.ts +0 -522
- package/src/ai.ts +0 -1388
package/src/studio/project.ts
CHANGED
|
@@ -34,6 +34,24 @@ export type SqlDialectKind = 'postgres' | 'mysql' | 'sqlite' | 'mssql' | 'supaba
|
|
|
34
34
|
/** In-memory (seeded) source. `seed` carries curated rows (e.g. a sample app);
|
|
35
35
|
* when absent the codegen + preview synthesize realistic rows. */
|
|
36
36
|
export type MemorySource = { kind: 'memory'; seed?: Record<string, unknown>[] }
|
|
37
|
+
/** Wire-format preset that teaches the REST source how a backend pages / sorts /
|
|
38
|
+
* wraps its rows, so the generated grid does REAL server-side paging + sort (not a
|
|
39
|
+
* single fetched page). Maps to the `offsetLimit` / `dummyjson` / `jsonServer`
|
|
40
|
+
* adapters in `@svgrid/enterprise`. Absent = the legacy manual `rowsPath`/`totalPath`
|
|
41
|
+
* (parse only; no server paging). */
|
|
42
|
+
export type RestAdapterConfig =
|
|
43
|
+
| { kind: 'dummyjson'; rowsKey?: string }
|
|
44
|
+
| { kind: 'jsonServer' }
|
|
45
|
+
| {
|
|
46
|
+
kind: 'offsetLimit'
|
|
47
|
+
offsetParam?: string
|
|
48
|
+
limitParam?: string
|
|
49
|
+
sortByParam?: string
|
|
50
|
+
orderParam?: string
|
|
51
|
+
searchParam?: string
|
|
52
|
+
rowsKey?: string
|
|
53
|
+
totalKey?: string
|
|
54
|
+
}
|
|
37
55
|
export type RestSource = {
|
|
38
56
|
kind: 'rest'
|
|
39
57
|
/** Origin + version prefix, e.g. `https://api.example.com/v1`. */
|
|
@@ -43,13 +61,16 @@ export type RestSource = {
|
|
|
43
61
|
method: RestMethod
|
|
44
62
|
params: RequestParam[]
|
|
45
63
|
idField?: string
|
|
64
|
+
/** Wire-format preset for real server paging/sort (offsetLimit / dummyjson /
|
|
65
|
+
* jsonServer). When set, it supersedes `rowsPath`/`totalPath`. */
|
|
66
|
+
adapter?: RestAdapterConfig
|
|
46
67
|
/** Dotted path in the response body holding the rows, e.g. `data.items`. */
|
|
47
68
|
rowsPath?: string
|
|
48
69
|
/** Dotted path holding the total row count, e.g. `data.total`. */
|
|
49
70
|
totalPath?: string
|
|
50
71
|
}
|
|
51
|
-
export type SqlSource = { kind: 'sql'; table: string; dialect?: SqlDialectKind }
|
|
52
|
-
export type SupabaseSource = { kind: 'supabase'; table: string; url?: string; key?: string }
|
|
72
|
+
export type SqlSource = { kind: 'sql'; table: string; dialect?: SqlDialectKind; /** DB schema / search path (Postgres/MSSQL); default `public`. Qualifies the table. */ schema?: string }
|
|
73
|
+
export type SupabaseSource = { kind: 'supabase'; table: string; url?: string; key?: string; realtime?: boolean }
|
|
53
74
|
/** Embedded Postgres (PGlite) - a real, persistent database with ZERO backend
|
|
54
75
|
* setup. Runs in the browser, persisting to IndexedDB. Swap for a `sql` source
|
|
55
76
|
* pointed at hosted Postgres to go to production (same schema + SQL). */
|
|
@@ -100,6 +121,15 @@ export type GridConfig = {
|
|
|
100
121
|
editing: GridEditing
|
|
101
122
|
/** Presentation of the edit form when `editing === 'form'`. */
|
|
102
123
|
formPresentation: Presentation
|
|
124
|
+
/** Form layout depth (all optional; default = the auto 2-column form of all fields). */
|
|
125
|
+
formColumns?: 1 | 2 | 3
|
|
126
|
+
/** Restrict + order the form's fields (field names). */
|
|
127
|
+
formFields?: string[]
|
|
128
|
+
/** Group fields into titled fieldsets. */
|
|
129
|
+
formSections?: Array<{ title?: string; fields: string[] }>
|
|
130
|
+
/** Dialog title override + width for the modal/drawer form. */
|
|
131
|
+
formTitle?: string
|
|
132
|
+
formSize?: 'sm' | 'md' | 'lg'
|
|
103
133
|
density: GridDensity
|
|
104
134
|
striped: boolean
|
|
105
135
|
/** Excel-style cell/range selection. */
|
|
@@ -135,6 +165,10 @@ export type GridConfig = {
|
|
|
135
165
|
* Kanban board). Mutually exclusive with grouping / tree. Uses the enterprise
|
|
136
166
|
* scheduler renderer (`enableSchedulerView`). */
|
|
137
167
|
scheduler?: SchedulerViewConfig
|
|
168
|
+
/** Raw SvGrid prop overrides from the block's "All properties" panel - every grid
|
|
169
|
+
* prop the curated controls don't manage. Passed straight through to `<SvGrid>`
|
|
170
|
+
* (deduped against the curated props at codegen time). */
|
|
171
|
+
props?: Record<string, unknown>
|
|
138
172
|
}
|
|
139
173
|
/** Which calendar view the scheduler opens on / offers. */
|
|
140
174
|
export type SchedulerViewMode = 'month' | 'week' | 'day' | 'agenda' | 'timelineDay' | 'timelineWeek' | 'timelineMonth' | 'timelineYear'
|
|
@@ -169,7 +203,20 @@ export type SchedulerViewConfig = {
|
|
|
169
203
|
export type TreeDataConfig = { parentField: string; labelField: string }
|
|
170
204
|
/** Which export affordances the grid toolbar shows. All go through the grid's built-in
|
|
171
205
|
* export API (dependency-free). Empty / all-false = no toolbar. */
|
|
172
|
-
|
|
206
|
+
/** Export buttons on a grid's toolbar.
|
|
207
|
+
*
|
|
208
|
+
* `csv` / `json` / `copy` use the free grid API. `xlsx` / `pdf` / `print` go
|
|
209
|
+
* through `@svgrid/enterprise` (real OOXML, pdfmake, paginated print) and pull
|
|
210
|
+
* in their optional peer deps - jszip for xlsx, pdfmake for pdf - which the
|
|
211
|
+
* generated package.json declares only when the button is switched on. */
|
|
212
|
+
export type GridExportConfig = {
|
|
213
|
+
csv?: boolean
|
|
214
|
+
json?: boolean
|
|
215
|
+
copy?: boolean
|
|
216
|
+
xlsx?: boolean
|
|
217
|
+
pdf?: boolean
|
|
218
|
+
print?: boolean
|
|
219
|
+
}
|
|
173
220
|
/** Which filtering surfaces the grid shows (when `filterable`). `global` = the search-all
|
|
174
221
|
* box, `row` = a filter input under each header, `menu` = the column header filter menu. */
|
|
175
222
|
export type GridFilterUi = { global?: boolean; row?: boolean; menu?: boolean }
|
|
@@ -360,7 +407,9 @@ export function blockClassName(block: Pick<Block, 'className'>): string {
|
|
|
360
407
|
}
|
|
361
408
|
/** The number of columns (1-12) a block occupies in the 12-col layout. */
|
|
362
409
|
export const blockColumns = (b: Pick<Block, 'span' | 'colSpan'>): number =>
|
|
363
|
-
|
|
410
|
+
// Default to full width when a block sets neither colSpan nor span (Copilot /
|
|
411
|
+
// hand-authored configs may omit both) - never emit `span NaN` into the HTML.
|
|
412
|
+
Math.max(1, Math.min(12, Math.round(b.colSpan ?? (b.span != null ? b.span * 4 : 12))))
|
|
364
413
|
|
|
365
414
|
/** Restrict a user-typed color to a safe subset so it can't break out of an inline
|
|
366
415
|
* `style="..."` attribute (hex, rgb()/hsl(), named colors, css vars). */
|
|
@@ -408,10 +457,49 @@ export const clickSlot = (blockId: string) => `click:${blockId}`
|
|
|
408
457
|
export const rowSelectSlot = (blockId: string) => `rowSelect:${blockId}`
|
|
409
458
|
/** The handler-steps key for a component block's change (value change) event. */
|
|
410
459
|
export const changeSlot = (blockId: string) => `change:${blockId}`
|
|
460
|
+
/** Generic event slot - `click`/`change` produce the same keys as clickSlot/changeSlot,
|
|
461
|
+
* so existing projects keep their wiring; any other declared component event
|
|
462
|
+
* (focus, select, toggle, ...) gets its own `<event>:<blockId>` slot. */
|
|
463
|
+
export const eventSlot = (event: string, blockId: string) => `${event}:${blockId}`
|
|
411
464
|
/** The handler-steps key for the screen's form-submit (record saved) event. The
|
|
412
465
|
* compiled steps get the submitted `row` (values) in scope. */
|
|
413
466
|
export const FORM_SUBMIT = 'formSubmit'
|
|
414
467
|
|
|
468
|
+
/** One event on the Grid, exposed to code-behind as `ctx.grid.<method> = (e) => {}`.
|
|
469
|
+
* `prop` is the SvGrid callback it wires to (empty for `dataEvent`s, which fire from
|
|
470
|
+
* Studio's data controller on create/update/delete rather than from the grid markup). */
|
|
471
|
+
export type GridEventDef = { key: string; method: string; prop: string; params: string; builtin: boolean; desc: string; dataEvent?: boolean }
|
|
472
|
+
/** The Grid's real event surface (mirrors SvGrid's `on*` callbacks). Studio wires
|
|
473
|
+
* every one into `ctx.grid` so page code can subscribe with `ctx.grid.onCellClick =
|
|
474
|
+
* (e) => {}`. `params` is the handler's parameter list, with `Row` as the row-type
|
|
475
|
+
* placeholder (substituted per screen). `builtin` marks events a Studio feature may
|
|
476
|
+
* already use (sort/filter/paginate/edit/row-click) - codegen COMPOSES the user's
|
|
477
|
+
* handler onto the built-in rather than emitting a duplicate prop. Signatures are
|
|
478
|
+
* transcribed from packages/grid/src/SvGrid.types.ts. */
|
|
479
|
+
export const GRID_EVENTS: readonly GridEventDef[] = [
|
|
480
|
+
{ key: 'rowClick', method: 'onRowClick', prop: 'onRowClick', builtin: true, desc: 'A data row is single-clicked.', params: 'e: { rowIndex: number; columnId: string; row: Row }' },
|
|
481
|
+
{ key: 'rowDoubleClick', method: 'onRowDoubleClick', prop: 'onRowDoubleClick', builtin: true, desc: 'A data row is double-clicked.', params: 'e: { rowIndex: number; columnId: string; row: Row }' },
|
|
482
|
+
{ key: 'cellClick', method: 'onCellClick', prop: 'onCellClick', builtin: false, desc: 'A data cell is single-clicked.', params: 'e: { rowIndex: number; colIndex: number; columnId: string; value: unknown; row: Row }' },
|
|
483
|
+
{ key: 'cellDoubleClick', method: 'onCellDoubleClick', prop: 'onCellDoubleClick', builtin: false, desc: 'A data cell is double-clicked.', params: 'e: { rowIndex: number; colIndex: number; columnId: string; value: unknown; row: Row }' },
|
|
484
|
+
{ key: 'rowSelectionChange', method: 'onRowSelectionChange', prop: 'onRowSelectionChange', builtin: false, desc: 'The row selection changes.', params: 'selection: Record<string, boolean>, rows: Row[]' },
|
|
485
|
+
{ key: 'cellSelectionChange', method: 'onCellSelectionChange', prop: 'onCellSelectionChange', builtin: false, desc: 'The cell-selection rectangle changes.', params: 'ranges: Array<[number, number, number, number]>' },
|
|
486
|
+
{ key: 'activeCellChange', method: 'onActiveCellChange', prop: 'onActiveCellChange', builtin: false, desc: 'The active cell changes.', params: 'cell: { rowIndex: number; colIndex: number; columnId: string }' },
|
|
487
|
+
{ key: 'cellValueChange', method: 'onCellValueChange', prop: 'onCellValueChange', builtin: true, desc: 'An inline cell edit is committed.', params: 'e: { rowIndex: number; columnId: string; oldValue: unknown; newValue: unknown; row: Row }' },
|
|
488
|
+
{ key: 'sortingChange', method: 'onSortingChange', prop: 'onSortingChange', builtin: true, desc: 'The sort clauses change.', params: 'sorting: Array<{ id: string; desc: boolean }>' },
|
|
489
|
+
{ key: 'filtersChange', method: 'onFiltersChange', prop: 'onFiltersChange', builtin: true, desc: 'Any in-grid filter changes.', params: 'filters: { global: string; columns: Array<{ id: string; operator: string; value: string; valueTo?: string; selectedValues?: string[] }> }' },
|
|
490
|
+
{ key: 'paginationChange', method: 'onPaginationChange', prop: 'onPaginationChange', builtin: true, desc: 'The page or page size changes.', params: 'pagination: { pageIndex: number; pageSize: number }' },
|
|
491
|
+
{ key: 'columnOrderChange', method: 'onColumnOrderChange', prop: 'onColumnOrderChange', builtin: false, desc: 'The column order changes.', params: 'order: ReadonlyArray<string>' },
|
|
492
|
+
{ key: 'scrollBottomReached', method: 'onScrollBottomReached', prop: 'onScrollBottomReached', builtin: false, desc: 'The body scrolls near the bottom (lazy-load hook).', params: 'e: { scrollTop: number; scrollHeight: number; clientHeight: number }' },
|
|
493
|
+
{ key: 'noteChange', method: 'onNoteChange', prop: 'onNoteChange', builtin: false, desc: 'A cell note/comment is saved or removed (needs editable comments).', params: 'e: { rowId: string; columnId: string; note: string }' },
|
|
494
|
+
{ key: 'rowDragEnd', method: 'onRowDragEnd', prop: 'onRowDragEnd', builtin: false, desc: 'A managed row drag settles (needs row dragging enabled).', params: 'e: { row: Row; toIndex: number; sameGrid: boolean; fromGridId: number; toGridId: number }' },
|
|
495
|
+
// Data-layer events - fired by the screen's data controller after a write settles
|
|
496
|
+
// (form save, inline edit, delete action, or ctx.data.create/update/delete), not by
|
|
497
|
+
// the grid markup. Only fire on entity screens (which have a data source).
|
|
498
|
+
{ key: 'rowAdded', method: 'onRowAdded', prop: '', builtin: false, dataEvent: true, desc: 'A row was created (form / ctx.data.create).', params: 'row: Row' },
|
|
499
|
+
{ key: 'rowUpdated', method: 'onRowUpdated', prop: '', builtin: false, dataEvent: true, desc: 'A row was updated (form / inline edit / ctx.data.update).', params: 'row: Row' },
|
|
500
|
+
{ key: 'rowDeleted', method: 'onRowDeleted', prop: '', builtin: false, dataEvent: true, desc: 'A row was deleted (delete action / ctx.data.delete).', params: 'id: string' },
|
|
501
|
+
]
|
|
502
|
+
|
|
415
503
|
// --- logic core: screen state + a small expression engine --------------------
|
|
416
504
|
|
|
417
505
|
/** A screen-scoped reactive variable (`ctx.state.<name>`). Emitted as `$state`. */
|
|
@@ -644,7 +732,7 @@ export function setLayoutOpts<K extends keyof LayoutOpts>(project: StudioProject
|
|
|
644
732
|
return mapScreen(project, screenId, (s) => ({ ...s, layoutOpts: { ...s.layoutOpts, [mode]: { ...s.layoutOpts?.[mode], ...patch } } }))
|
|
645
733
|
}
|
|
646
734
|
|
|
647
|
-
export type Screen = { id: string; entity?: string; title: string; route: string; blocks: Block[]; nav?: ScreenNav; actions?: ActionConfig[]; code?: boolean; renderGrid?: boolean; handlerBodies?: Record<string, string>; handlerSteps?: Record<string, ActionStep[]>; handlersSource?: string; className?: string; layout?: ScreenLayout; dock?: DockManagerState; canvas?: Record<string, CanvasRect>; layoutOpts?: LayoutOpts; state?: StateVar[] }
|
|
735
|
+
export type Screen = { id: string; entity?: string; title: string; route: string; blocks: Block[]; nav?: ScreenNav; actions?: ActionConfig[]; code?: boolean; renderGrid?: boolean; handlerBodies?: Record<string, string>; handlerSteps?: Record<string, ActionStep[]>; handlersSource?: string; className?: string; layout?: ScreenLayout; dock?: DockManagerState; canvas?: Record<string, CanvasRect>; layoutOpts?: LayoutOpts; state?: StateVar[]; renderMode?: 'ssr' | 'spa' }
|
|
648
736
|
|
|
649
737
|
/** The generated app's shell (master layout): sidebar, top-nav, or bottom-nav; brand, footer. */
|
|
650
738
|
export type ShellStyle = 'sidebar' | 'top-nav' | 'bottom-nav'
|
|
@@ -680,6 +768,13 @@ export type AccessControl = {
|
|
|
680
768
|
export type OAuthProvider = 'github' | 'google' | 'oidc'
|
|
681
769
|
export type AuthConfig = {
|
|
682
770
|
enabled: boolean
|
|
771
|
+
/** Which sign-in system to scaffold. `'builtin'` (default) is the dependency-free
|
|
772
|
+
* cookie-session starter below (own user store, server route guards, RBAC).
|
|
773
|
+
* `'supabase'` delegates to Supabase Auth: a client-side `SvAuthGate` over the
|
|
774
|
+
* shared Supabase client, pairing with the database's Row Level Security. The
|
|
775
|
+
* builtin-only sub-options (register / userAdmin / oauth / twoFactor / email) do
|
|
776
|
+
* not apply to the Supabase provider. */
|
|
777
|
+
provider?: 'builtin' | 'supabase'
|
|
683
778
|
/** Redirect unauthenticated visitors to /login for every route. Default true. */
|
|
684
779
|
protect?: boolean
|
|
685
780
|
/** Self-service sign-up + password recovery (/register, /forgot-password,
|
|
@@ -711,6 +806,10 @@ export type StudioProject = {
|
|
|
711
806
|
dataSource: DataSourceKind
|
|
712
807
|
/** Per-entity data-source binding, keyed by entity name. */
|
|
713
808
|
dataSources?: Record<string, EntityDataSource>
|
|
809
|
+
/** Project-level Supabase connection (URL + anon key), shared by every
|
|
810
|
+
* Supabase-bound entity so it's set once. A per-entity `SupabaseSource.url`/`key`
|
|
811
|
+
* still overrides for that entity. Read by the wizard + the `.env` paste hint. */
|
|
812
|
+
supabase?: { url?: string; key?: string }
|
|
714
813
|
theme?: ProjectTheme
|
|
715
814
|
/** Role-based access control (optional; off unless `access.enabled`). */
|
|
716
815
|
access?: AccessControl
|
|
@@ -731,6 +830,117 @@ export type StudioProject = {
|
|
|
731
830
|
/** Server-side business-rule triggers, keyed by entity name. Enforced on the
|
|
732
831
|
* SQL route (compiled into createKitHandlers `hooks`). */
|
|
733
832
|
triggers?: Record<string, EntityTriggers>
|
|
833
|
+
/** Scheduled background jobs: a guarded `/api/cron` endpoint plus the schedule
|
|
834
|
+
* config for the deploy target. Empty / omitted emits nothing. */
|
|
835
|
+
jobs?: ScheduledJob[]
|
|
836
|
+
/** Multi-tenancy: scope every row to the signed-in user's tenant. */
|
|
837
|
+
tenancy?: TenancyConfig
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Multi-tenancy for the generated app: one database, one deployment, rows
|
|
842
|
+
* partitioned by a tenant column.
|
|
843
|
+
*
|
|
844
|
+
* Enforced on the SERVER, in the API route's `scope` option - reads are
|
|
845
|
+
* filtered, creates are stamped, and updates/deletes re-read the target under
|
|
846
|
+
* the scope first. A client that skips the UI still cannot reach another
|
|
847
|
+
* tenant's rows.
|
|
848
|
+
*
|
|
849
|
+
* Requires the [auth starter](./auth.md) (the tenant comes from the session)
|
|
850
|
+
* and the typed data layer (the column has to exist in the schema). Without
|
|
851
|
+
* both it degrades to off rather than emitting a half-enforced scope.
|
|
852
|
+
*/
|
|
853
|
+
export type TenancyConfig = {
|
|
854
|
+
enabled: boolean
|
|
855
|
+
/** Column holding the tenant key on every scoped entity. Default `tenantId`. */
|
|
856
|
+
field?: string
|
|
857
|
+
/** Entities to leave GLOBAL (shared across tenants) - reference/lookup tables
|
|
858
|
+
* like currencies or countries that every tenant reads. */
|
|
859
|
+
sharedEntities?: string[]
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/** What a scheduled job does when it fires.
|
|
863
|
+
* - `email`: send a summary of an entity (row count + the newest rows).
|
|
864
|
+
* - `code`: run a body you write, with `db` / `sendEmail` in scope. */
|
|
865
|
+
export type ScheduledJobKind = 'email' | 'code'
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* One scheduled background job.
|
|
869
|
+
*
|
|
870
|
+
* Jobs run on the SERVER via a generated `/api/cron` route, not in a browser
|
|
871
|
+
* tab - unlike the client-side `createScheduler` in `@svgrid/enterprise`, which
|
|
872
|
+
* only fires while the app is open. The route is secret-guarded and invoked by
|
|
873
|
+
* the platform's scheduler (Vercel Cron, a GitHub Actions schedule, or your own
|
|
874
|
+
* crontab hitting the URL).
|
|
875
|
+
*/
|
|
876
|
+
export type ScheduledJob = {
|
|
877
|
+
/** Stable id - the `?job=` selector and the handler name. */
|
|
878
|
+
id: string
|
|
879
|
+
/** Human label used in comments + the DEPLOY.md table. */
|
|
880
|
+
name: string
|
|
881
|
+
/** Standard 5-field cron expression, interpreted in UTC. */
|
|
882
|
+
cron: string
|
|
883
|
+
/** Off by default is surprising for something you explicitly added, so a job
|
|
884
|
+
* runs unless `enabled` is explicitly false. */
|
|
885
|
+
enabled?: boolean
|
|
886
|
+
kind: ScheduledJobKind
|
|
887
|
+
/** `email`: the entity to summarize. */
|
|
888
|
+
entity?: string
|
|
889
|
+
/** `email`: recipient address. */
|
|
890
|
+
to?: string
|
|
891
|
+
/** `email`: subject line. Defaults to the job name. */
|
|
892
|
+
subject?: string
|
|
893
|
+
/** `code`: the handler body (TypeScript). */
|
|
894
|
+
code?: string
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/** The render mode for a screen. `'ssr'` emits idiomatic SvelteKit (`+page.server.ts`
|
|
898
|
+
* with a `load` + form `actions`, SSR + progressive enhancement); `'spa'` (the
|
|
899
|
+
* default) emits the client data-source-controller page. Opt-in for now. */
|
|
900
|
+
export function screenRenderMode(_project: StudioProject, screen: Screen): 'ssr' | 'spa' {
|
|
901
|
+
return screen.renderMode === 'ssr' ? 'ssr' : 'spa'
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/** Block kinds a read-only SSR screen can host: pure renders over server-loaded
|
|
905
|
+
* rows. Board/calendar/scheduler (client interaction runtimes), components
|
|
906
|
+
* (client bindings), grids-with-extras, and containers stay SPA. */
|
|
907
|
+
const SSR_READ_KINDS = new Set<BlockKind>(['chart', 'pivot', 'dashboard', 'kpi', 'gauge', 'tree', 'detail', 'master-detail'])
|
|
908
|
+
|
|
909
|
+
/** How a screen would emit under SSR: 'grid' (single grid -> load + form actions,
|
|
910
|
+
* URL-driven sort/filter/page), 'read' (data-viz/detail blocks -> load only), or
|
|
911
|
+
* null when it must stay SPA. */
|
|
912
|
+
export function ssrScreenShape(project: StudioProject, screen: Screen): 'grid' | 'read' | null {
|
|
913
|
+
if (!screen.entity || screen.code) return null
|
|
914
|
+
const kind = project.dataSources?.[screen.entity]?.kind ?? project.dataSource
|
|
915
|
+
// memory runs the source in-process; sql reuses the connected /api route via
|
|
916
|
+
// event.fetch. (rest/supabase/pglite stay SPA for now.)
|
|
917
|
+
if (kind !== 'memory' && kind !== 'sql') return null
|
|
918
|
+
const blocks = screen.blocks ?? []
|
|
919
|
+
if (blocks.length === 1 && blocks[0]!.config.kind === 'grid') {
|
|
920
|
+
const g = blocks[0]!.config as GridConfig
|
|
921
|
+
if (g.treeData || g.scheduler) return null
|
|
922
|
+
return 'grid'
|
|
923
|
+
}
|
|
924
|
+
if (blocks.length >= 1 && blocks.every((b) => SSR_READ_KINDS.has(b.config.kind))) return 'read'
|
|
925
|
+
return null
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/** Whether a screen can be emitted as SSR-native (any supported shape). */
|
|
929
|
+
export function ssrEligible(project: StudioProject, screen: Screen): boolean {
|
|
930
|
+
return ssrScreenShape(project, screen) !== null
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/** Set a screen's render mode ('spa' clears back to the default). */
|
|
934
|
+
export function setScreenRenderMode(project: StudioProject, screenId: string, mode: 'ssr' | 'spa'): StudioProject {
|
|
935
|
+
return {
|
|
936
|
+
...project,
|
|
937
|
+
screens: project.screens.map((s) => (s.id === screenId ? { ...s, renderMode: mode === 'ssr' ? 'ssr' : undefined } : s)),
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/** True when the screen should actually emit as SSR (mode is 'ssr' AND eligible). */
|
|
942
|
+
export function isSsrScreen(project: StudioProject, screen: Screen): boolean {
|
|
943
|
+
return screenRenderMode(project, screen) === 'ssr' && ssrEligible(project, screen)
|
|
734
944
|
}
|
|
735
945
|
|
|
736
946
|
/** The triggers configured for an entity (or an empty object). */
|
|
@@ -749,6 +959,32 @@ export function setTrigger(project: StudioProject, entity: string, event: Trigge
|
|
|
749
959
|
return { ...project, triggers: Object.keys(triggers).length ? triggers : undefined }
|
|
750
960
|
}
|
|
751
961
|
|
|
962
|
+
/** Turn multi-tenancy on/off and configure the scoping column. */
|
|
963
|
+
export function setTenancy(project: StudioProject, tenancy: TenancyConfig | null): StudioProject {
|
|
964
|
+
if (!tenancy || !tenancy.enabled) {
|
|
965
|
+
const { tenancy: _drop, ...rest } = project
|
|
966
|
+
return rest
|
|
967
|
+
}
|
|
968
|
+
return { ...project, tenancy }
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/** The tenant column name, defaulted. */
|
|
972
|
+
export const tenantField = (project: StudioProject): string => project.tenancy?.field || 'tenantId'
|
|
973
|
+
|
|
974
|
+
/** True when `entity` is scoped to a tenant (i.e. tenancy is on and it is not
|
|
975
|
+
* listed as shared). Shared entities stay global reference data. */
|
|
976
|
+
export function isTenantScoped(project: StudioProject, entity: string): boolean {
|
|
977
|
+
if (project.tenancy?.enabled !== true) return false
|
|
978
|
+
return !(project.tenancy.sharedEntities ?? []).includes(entity)
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/** Add or replace a scheduled job (matched by `id`). Pass `null` to remove one. */
|
|
982
|
+
export function setJob(project: StudioProject, id: string, job: Omit<ScheduledJob, 'id'> | null): StudioProject {
|
|
983
|
+
const rest = (project.jobs ?? []).filter((j) => j.id !== id)
|
|
984
|
+
const next = job ? [...rest, { ...job, id }] : rest
|
|
985
|
+
return { ...project, jobs: next.length ? next : undefined }
|
|
986
|
+
}
|
|
987
|
+
|
|
752
988
|
/** Where the generated app deploys. Drives the emitted SvelteKit adapter + config. */
|
|
753
989
|
export type DeployTarget = 'auto' | 'vercel' | 'netlify' | 'cloudflare' | 'node'
|
|
754
990
|
|
|
@@ -1243,7 +1479,13 @@ export function removeScreen(project: StudioProject, screenId: string): StudioPr
|
|
|
1243
1479
|
return { ...project, screens: project.screens.filter((s) => s.id !== screenId) }
|
|
1244
1480
|
}
|
|
1245
1481
|
|
|
1246
|
-
export function updateScreen(
|
|
1482
|
+
export function updateScreen(
|
|
1483
|
+
project: StudioProject,
|
|
1484
|
+
screenId: string,
|
|
1485
|
+
// `renderMode` is patchable here so SSR output is reachable without the
|
|
1486
|
+
// designer - the CLI and the MCP tools drive the model through this function.
|
|
1487
|
+
patch: Partial<Pick<Screen, 'title' | 'route' | 'entity' | 'nav' | 'actions' | 'className' | 'renderMode'>>,
|
|
1488
|
+
): StudioProject {
|
|
1247
1489
|
return mapScreen(project, screenId, (s) => ({ ...s, ...patch }))
|
|
1248
1490
|
}
|
|
1249
1491
|
|
|
@@ -1761,10 +2003,12 @@ export function setAuth(project: StudioProject, patch: Partial<AuthConfig> & { e
|
|
|
1761
2003
|
if (!patch.enabled) { const { auth: _drop, ...rest } = project; return rest }
|
|
1762
2004
|
const prev = project.auth
|
|
1763
2005
|
const oauth = patch.oauth ?? prev?.oauth
|
|
2006
|
+
const provider = patch.provider ?? prev?.provider
|
|
1764
2007
|
return {
|
|
1765
2008
|
...project,
|
|
1766
2009
|
auth: {
|
|
1767
2010
|
enabled: true,
|
|
2011
|
+
...(provider && provider !== 'builtin' ? { provider } : {}),
|
|
1768
2012
|
protect: patch.protect ?? prev?.protect ?? true,
|
|
1769
2013
|
register: patch.register ?? prev?.register ?? false,
|
|
1770
2014
|
userAdmin: patch.userAdmin ?? prev?.userAdmin ?? false,
|
|
@@ -1812,9 +2056,11 @@ export function setEntityDataSource(project: StudioProject, entityName: string,
|
|
|
1812
2056
|
return { ...project, dataSources: { ...project.dataSources, [entityName]: source } }
|
|
1813
2057
|
}
|
|
1814
2058
|
|
|
1815
|
-
/** The resolved source for an entity
|
|
2059
|
+
/** The resolved source for an entity: its explicit per-entity binding, else a skeleton
|
|
2060
|
+
* for the project's default source kind (so the rail "Default source" actually applies
|
|
2061
|
+
* to unbound entities, and this agrees with `ssrScreenShape`'s fallback). */
|
|
1816
2062
|
export function entityDataSource(project: StudioProject, entityName: string): EntityDataSource {
|
|
1817
|
-
return project.dataSources?.[entityName] ??
|
|
2063
|
+
return project.dataSources?.[entityName] ?? defaultEntitySource(project.dataSource ?? 'memory', entityName)
|
|
1818
2064
|
}
|
|
1819
2065
|
|
|
1820
2066
|
export function setTheme(project: StudioProject, theme: ProjectTheme): StudioProject {
|
|
@@ -1972,6 +2218,7 @@ export function parseProject(json: string): StudioProject {
|
|
|
1972
2218
|
screens: p.screens as Screen[],
|
|
1973
2219
|
dataSource: (p.dataSource ?? 'memory') as DataSourceKind,
|
|
1974
2220
|
...(p.dataSources && typeof p.dataSources === 'object' ? { dataSources: p.dataSources as Record<string, EntityDataSource> } : {}),
|
|
2221
|
+
...(p.supabase && typeof p.supabase === 'object' ? { supabase: p.supabase as { url?: string; key?: string } } : {}),
|
|
1975
2222
|
...(p.theme && typeof p.theme === 'object' ? { theme: p.theme as ProjectTheme } : {}),
|
|
1976
2223
|
...(p.access && typeof p.access === 'object' ? { access: p.access as AccessControl } : {}),
|
|
1977
2224
|
...(p.auth && typeof p.auth === 'object' && (p.auth as AuthConfig).enabled ? { auth: p.auth as AuthConfig } : {}),
|
|
@@ -2028,6 +2275,53 @@ export function validateProject(project: StudioProject): ProjectIssue[] {
|
|
|
2028
2275
|
}
|
|
2029
2276
|
}
|
|
2030
2277
|
}
|
|
2278
|
+
|
|
2279
|
+
// Project-level deployment footguns (all advisory - the app still generates).
|
|
2280
|
+
const sources = Object.values(project.dataSources ?? {})
|
|
2281
|
+
const sqlSources = sources.filter((s): s is Extract<EntityDataSource, { kind: 'sql' }> => s.kind === 'sql')
|
|
2282
|
+
if (sqlSources.length > 0 && project.dataLayer !== 'drizzle') {
|
|
2283
|
+
issues.push({
|
|
2284
|
+
level: 'warning',
|
|
2285
|
+
message:
|
|
2286
|
+
'SQL entities have no migrations - their tables must already exist in the database. ' +
|
|
2287
|
+
'Enable the Drizzle data layer for typed migrations, or run the generated db/schema.sql against your database first.',
|
|
2288
|
+
})
|
|
2289
|
+
}
|
|
2290
|
+
if (project.deploy === 'cloudflare' && sqlSources.some((s) => s.dialect === 'postgres' || s.dialect === 'supabase')) {
|
|
2291
|
+
issues.push({
|
|
2292
|
+
level: 'warning',
|
|
2293
|
+
message:
|
|
2294
|
+
'Cloudflare Workers cannot run the socket "pg" Postgres driver. Swap the generated route to an HTTP driver ' +
|
|
2295
|
+
'(e.g. Neon serverless) or choose another deploy target.',
|
|
2296
|
+
})
|
|
2297
|
+
}
|
|
2298
|
+
for (const [name, src] of Object.entries(project.dataSources ?? {})) {
|
|
2299
|
+
if (src.kind === 'supabase' && !(src.url ?? project.supabase?.url) && !(src.key ?? project.supabase?.key)) {
|
|
2300
|
+
issues.push({
|
|
2301
|
+
level: 'warning',
|
|
2302
|
+
message: `Supabase entity "${name}" has no URL/key - set the shared project connection (or this entity's own), or the generated app just reads PUBLIC_SUPABASE_URL / PUBLIC_SUPABASE_ANON_KEY from .env.`,
|
|
2303
|
+
})
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
// Supabase Auth needs a Supabase client to authenticate against: either the shared
|
|
2307
|
+
// project connection or at least one Supabase-bound entity (both emit connections.ts).
|
|
2308
|
+
if (project.auth?.enabled && project.auth.provider === 'supabase') {
|
|
2309
|
+
const hasSupabase = !!project.supabase?.url || sources.some((s) => s.kind === 'supabase')
|
|
2310
|
+
if (!hasSupabase) {
|
|
2311
|
+
issues.push({
|
|
2312
|
+
level: 'warning',
|
|
2313
|
+
message: 'Supabase Auth needs a Supabase connection. Set the shared project URL / anon key (in a data-source builder), or bind an entity to Supabase.',
|
|
2314
|
+
})
|
|
2315
|
+
}
|
|
2316
|
+
// Supabase Auth is a client-side gate; it does not populate the server-side role
|
|
2317
|
+
// the RBAC route guard reads. Server enforcement must come from RLS instead.
|
|
2318
|
+
if (project.access?.enabled) {
|
|
2319
|
+
issues.push({
|
|
2320
|
+
level: 'warning',
|
|
2321
|
+
message: 'Supabase Auth signs in on the client, so it does not populate the server-side role that RBAC route guards check. Enforce per-user access with Row Level Security policies on your Supabase tables.',
|
|
2322
|
+
})
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2031
2325
|
return issues
|
|
2032
2326
|
}
|
|
2033
2327
|
|