@svgrid/enterprise 3.1.4 → 3.1.5

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 (35) hide show
  1. package/dist/cdn/svgrid-enterprise.svelte-external.js +8956 -8606
  2. package/dist/designer/assets/GridMenus-CvcASlKv.js +8 -0
  3. package/dist/designer/assets/SvListBox-BlmmzYcU.js +110 -0
  4. package/dist/designer/assets/{index-BgVTSVoK.js → index-B3WBezq_.js} +432 -370
  5. package/dist/designer/assets/index-KmSEY7Lb.css +1 -0
  6. package/dist/designer/assets/src-D4PRZJYA.js +58 -0
  7. package/dist/designer/assets/{validate-YxSrBF1H.js → validate-B71dE1Cs.js} +1 -1
  8. package/dist/designer/index.html +4 -4
  9. package/dist/node/studio.js +402 -81
  10. package/dist/server/server-row-model.js +199 -26
  11. package/dist/studio/emit-project.js +86 -56
  12. package/dist/studio/emit-schema.d.ts +3 -0
  13. package/dist/studio/emit-schema.js +128 -29
  14. package/dist/studio/index.d.ts +2 -1
  15. package/dist/studio/index.js +2 -1
  16. package/dist/studio/init-flow.d.ts +3 -0
  17. package/dist/studio/init-flow.js +5 -2
  18. package/dist/studio/project.d.ts +10 -1
  19. package/dist/studio/project.js +28 -15
  20. package/dist/studio/samples/restaurant.js +1 -1
  21. package/dist/studio/sveltekit3.d.ts +38 -0
  22. package/dist/studio/sveltekit3.js +194 -0
  23. package/dist/studio/ui-components.generated.js +15 -4
  24. package/dist/version.d.ts +2 -2
  25. package/dist/version.js +2 -2
  26. package/dist/watermark.js +31 -1
  27. package/dist/wc/{GridMenus-6_fF5jIm.js → GridMenus-fWbBUiRH.js} +121 -121
  28. package/dist/wc/sv-sheet-element.js +10979 -10522
  29. package/dist/wc/{svelte-virtualizer.svelte-Du4tjrIP.js → svelte-virtualizer.svelte-Dntdh8Ms.js} +103 -56
  30. package/dist/wc/{validate-JAuMsk6Z.js → validate-43C03DLP.js} +1 -1
  31. package/package.json +3 -3
  32. package/dist/designer/assets/GridMenus-DGZJKFvX.js +0 -8
  33. package/dist/designer/assets/SvListBox-v2jm0-vf.js +0 -110
  34. package/dist/designer/assets/index-l21iGkpF.css +0 -1
  35. package/dist/designer/assets/src-BqEXoOog.js +0 -58
@@ -1,10 +1,11 @@
1
+ import { toSvelteKit3 } from './sveltekit3.js';
1
2
  import { tenantField, isTenantScoped } from './project.js';
2
3
  import { blockColumns, blockStyleCss, blockClassName, sanitizeClassName, componentHandleName, componentHasBindings, entityDataSource, flattenBlocks, serializeProject, seedUsers, compileHandlerSteps, rowSelectSlot, eventSlot, FORM_SUBMIT, GRID_EVENTS, screenLayoutOf, isPaneLayout, canvasRectOf, CANVAS_ROW_PX, CANVAS_GAP_PX, gridOpts, stackOpts, splitOpts, dockOpts, canvasOpts, stateInitExpr, stateTsType, reconcileDock, ON_LOAD, ON_DESTROY, isSsrScreen, ssrScreenShape } from './project.js';
3
4
  import { uiComponentSpec, gridApiSettableProps, STANDARD_UI_EVENTS } from './ui-components.js';
4
5
  import { isDarkTheme, themeTokenCss } from './themes.js';
5
6
  import { hasFieldConditions } from '../edit-panel.js';
6
7
  import { SVGRID_VERSION, GRID_VERSION } from '../version.js';
7
- import { emitEntityModules, homeFile, layoutFile, lookupVar, namesFor, prepareEntities, relationDisplayFields, sqlDdlFiles } from './emit-schema.js';
8
+ import { emitEntityModules, homeFile, layoutFile, lookupVar, namesFor, prepareEntities, relationDisplayFields, sqlDdlFiles, storeVar } from './emit-schema.js';
8
9
  const has = (blocks, kind) => blocks.some((b) => b.config.kind === kind);
9
10
  /** Per-Tabs-block active-tab state var + a stable tab id. */
10
11
  const tabsStateVar = (blockId) => `activeTab_${blockId.replace(/[^a-zA-Z0-9_$]/g, '_')}`;
@@ -2423,27 +2424,28 @@ function screenChildEntities(blocks, resolve) {
2423
2424
  }
2424
2425
  return [...out.values()];
2425
2426
  }
2426
- function ssrReadServerFile(schema, screen, sourceKind, accessEnabled, screenIds, resolve) {
2427
- const n = namesFor(schema);
2428
- const isSql = sourceKind === 'sql';
2427
+ function ssrReadServerFile(schema, screen, routed, accessEnabled, screenIds, resolve) {
2429
2428
  const children = screenChildEntities(screen.blocks, resolve);
2430
- const rbac = accessEnabled && !isSql; // sql: the /api route already enforces authz
2431
- const srcExpr = (s) => isSql ? `createKitDataSource<Record<string, unknown>>({ endpoint: ${jsStr('/api/' + namesFor(s).route)}, fetch })` : namesFor(s).sourceVar;
2432
- const memImports = isSql ? [] : [...new Set([n.sourceVar, ...children.map((c) => namesFor(c).sourceVar)])];
2429
+ const viaRoute = routed(schema.name);
2430
+ const rbac = accessEnabled && !viaRoute; // a routed entity's /api route already enforces authz
2431
+ const srcExpr = (s) => routed(s.name) ? `createKitDataSource<Record<string, unknown>>({ endpoint: ${jsStr('/api/' + namesFor(s).route)}, fetch })` : namesFor(s).sourceVar;
2432
+ const all = [schema, ...children];
2433
+ const anyRouted = all.some((s) => routed(s.name));
2434
+ const dataImports = [...new Set(all.filter((s) => !routed(s.name)).map((s) => namesFor(s).sourceVar))];
2433
2435
  const childLoads = children.map((c) => ` const ${mdChildVar(c.name)} = (await ${srcExpr(c)}.getRows(PAGE)).rows`).join('\n');
2434
2436
  return {
2435
2437
  path: `src/routes/${screen.route}/+page.server.ts`,
2436
2438
  description: `${screen.title} - SSR load (full dataset${children.length ? ' + child collections' : ''}).`,
2437
2439
  contents: `import type { PageServerLoad } from './$types'
2438
2440
  import type { ServerRequest } from '@svgrid/grid'
2439
- ${isSql ? "import { createKitDataSource } from '@svgrid/enterprise'\n" : ''}${rbac ? "import { error } from '@sveltejs/kit'\nimport { authorizeAction, getServerRole } from '$lib/access'\n" : ''}${memImports.length ? `import { ${memImports.join(', ')} } from '$lib/data'\n` : ''}
2441
+ ${anyRouted ? "import { createKitDataSource } from '@svgrid/enterprise'\n" : ''}${rbac ? "import { error } from '@sveltejs/kit'\nimport { authorizeAction, getServerRole } from '$lib/access'\n" : ''}${dataImports.length ? `import { ${dataImports.join(', ')} } from '$lib/data'\n` : ''}
2440
2442
  // This screen renders on the server: real SSR HTML, not just a server-side
2441
2443
  // load. Stated explicitly so it holds whichever way the root layout is set.
2442
2444
  export const ssr = true
2443
2445
  ${rbac ? `\nconst SCREEN_IDS = ${JSON.stringify(screenIds)}\n` : ''}
2444
2446
  const PAGE: ServerRequest = { startRow: 0, endRow: 1000, pageIndex: 0, pageSize: 1000, sortModel: [], filterModel: {} }
2445
2447
 
2446
- export const load: PageServerLoad = async (${isSql || rbac ? `{ ${[isSql ? 'fetch' : '', rbac ? 'locals' : ''].filter(Boolean).join(', ')} }` : ''}) => {
2448
+ export const load: PageServerLoad = async (${anyRouted || rbac ? `{ ${[anyRouted ? 'fetch' : '', rbac ? 'locals' : ''].filter(Boolean).join(', ')} }` : ''}) => {
2447
2449
  ${rbac ? ` if (!authorizeAction(getServerRole({ locals }), 'read', SCREEN_IDS)) throw error(403, 'Not allowed')\n` : ''} const rows = (await ${srcExpr(schema)}.getRows(PAGE)).rows
2448
2450
  ${childLoads ? childLoads + '\n' : ''} return { rows${children.map((c) => `, ${mdChildVar(c.name)}`).join('')} }
2449
2451
  }
@@ -2525,13 +2527,13 @@ function attrs(f) {
2525
2527
  return out.length ? ' ' + out.join(' ') : '';
2526
2528
  }
2527
2529
  /** The `+page.server.ts` + SSR `+page.svelte` for a single-grid CRUD screen. */
2528
- function emitSsrGridScreen(schema, screen, sourceKind, accessEnabled, screenIds, byName) {
2530
+ function emitSsrGridScreen(schema, screen, routed, accessEnabled, screenIds, byName) {
2529
2531
  const n = namesFor(schema);
2530
- const isSql = sourceKind === 'sql';
2531
- // REST reads like the memory path - the source is imported from $lib/data and
2532
- // called directly - but the remote API owns the ids, so a create posts the
2533
- // values alone, as sql does.
2534
- const isRest = sourceKind === 'rest';
2532
+ // Routed (SQL, or in-memory rows held on the server): a same-origin client over
2533
+ // the entity's /api route. Otherwise REST, whose source is imported from
2534
+ // $lib/data and called directly. Either way the backend owns the ids, so a
2535
+ // create posts the values alone.
2536
+ const isSql = routed(schema.name);
2535
2537
  const relSchemaOf = (f) => byName.get(f.relation.entity);
2536
2538
  const relIdOf = (rs) => rs.idField ?? rs.fields.find((x) => x.primaryKey)?.field ?? 'id';
2537
2539
  const grid = screen.blocks.find((b) => b.config.kind === 'grid').config;
@@ -2550,33 +2552,33 @@ function emitSsrGridScreen(schema, screen, sourceKind, accessEnabled, screenIds,
2550
2552
  const normType = (t) => (t === 'number' ? 'number' : t === 'boolean' ? 'boolean' : 'text');
2551
2553
  const fieldTypesLit = `{ ${formFields.map((f) => `${jsStr(f.field)}: ${jsStr(normType(f.type))}`).join(', ')} }`;
2552
2554
  // Relation fields render as a native <select> whose options are prefetched in
2553
- // load - from the in-process source on memory, or the related entity's /api
2554
- // route on sql (see relSource below).
2555
+ // load - from the related entity's /api route when it has one, else from its
2556
+ // in-process source in $lib/data (see relSource below).
2555
2557
  const isRel = (f) => f.type === 'relation' && !!f.relation && byName.has(f.relation.entity);
2556
2558
  const relFields = formFields.filter(isRel);
2557
- // The $lib/data import only needs the related sources on the memory path; on sql
2558
- // the related options come from the related entity's /api route (via event.fetch).
2559
- const relSourceVars = isSql ? [] : [...new Set(relFields.map((f) => namesFor(relSchemaOf(f)).sourceVar))];
2559
+ const relRouted = relFields.some((f) => routed(relSchemaOf(f).name));
2560
+ const relSourceVars = [...new Set(relFields.filter((f) => !routed(relSchemaOf(f).name)).map((f) => namesFor(relSchemaOf(f)).sourceVar))];
2560
2561
  const relPrefetch = relFields
2561
2562
  .map((f) => {
2562
2563
  const rs = relSchemaOf(f);
2563
2564
  const rel = namesFor(rs);
2564
2565
  const relId = relIdOf(rs);
2565
2566
  const labelF = f.relation.labelField ?? relId;
2566
- const relSource = isSql
2567
+ const relSource = routed(rs.name)
2567
2568
  ? `createKitDataSource<Record<string, unknown>>({ endpoint: ${jsStr('/api/' + rel.route)}, fetch })`
2568
2569
  : rel.sourceVar;
2569
2570
  return ` const ${f.field}Options = (await ${relSource}.getRows({ startRow: 0, endRow: 100, pageIndex: 0, pageSize: 100, sortModel: [], filterModel: {} })).rows.map((r: Record<string, unknown>) => ({ value: String(r[${jsStr(relId)}] ?? ''), label: String(r[${jsStr(labelF)}] ?? '') }))`;
2570
2571
  })
2571
2572
  .join('\n');
2572
2573
  const relReturn = relFields.map((f) => `, ${f.field}Options`).join('');
2573
- // Source acquisition differs by kind:
2574
- // - memory: import the in-process source from $lib/data and call it directly.
2575
- // - sql: build a same-origin client over the connected /api/<entity> route with
2576
- // SvelteKit's event.fetch, so validation / RBAC / triggers / audit stay enforced
2577
- // once, in that route's createKitHandlers - not duplicated here.
2574
+ // Source acquisition:
2575
+ // - routed: build a same-origin client over the entity's /api/<entity> route
2576
+ // with SvelteKit's event.fetch, so validation / RBAC / triggers / audit stay
2577
+ // enforced once, in that route's createKitHandlers - not duplicated here.
2578
+ // - REST: import the source from $lib/data and call the remote API directly.
2578
2579
  const src = isSql ? 'source(fetch)' : n.sourceVar;
2579
2580
  const fetchArg = isSql ? ', fetch' : '';
2581
+ const loadFetchArg = isSql || relRouted ? ', fetch' : '';
2580
2582
  // A schema with value-driven fields posts through `stripHiddenValues`, so a
2581
2583
  // field the form was hiding is never written from a submitted FormData - the
2582
2584
  // same fields the client panel would have sent.
@@ -2585,21 +2587,17 @@ function emitSsrGridScreen(schema, screen, sourceKind, accessEnabled, screenIds,
2585
2587
  const enterpriseImports = [
2586
2588
  'validateAll',
2587
2589
  ...(conditional ? ['stripHiddenValues'] : []),
2588
- ...(isSql ? ['createKitDataSource'] : []),
2590
+ ...(isSql || relRouted ? ['createKitDataSource'] : []),
2589
2591
  ].join(', ');
2590
- const sourceImport = isSql
2591
- ? ''
2592
- : `import { ${[...new Set([n.sourceVar, ...relSourceVars]), ...(isRest ? [] : ['nextId'])].join(', ')} } from '$lib/data'\n`;
2592
+ const dataImports = [...new Set([...(isSql ? [] : [n.sourceVar]), ...relSourceVars])];
2593
+ const sourceImport = dataImports.length ? `import { ${dataImports.join(', ')} } from '$lib/data'\n` : '';
2593
2594
  const schemaImport = `import { ${n.schemaVar}${isSql ? `, type ${n.type}` : ''} } from '$lib/schemas'`;
2594
- const idConst = isSql || isRest ? '' : `\nconst ID_FIELD = ${jsStr(idField)}`;
2595
2595
  const srcHelper = isSql
2596
2596
  ? `\n// Same-origin client over the connected /api/${n.route} route (that route runs\n// validation, RBAC, triggers + audit via createKitHandlers).\nconst source = (fetch: typeof globalThis.fetch) => createKitDataSource<${n.type}>({ endpoint: ${jsStr('/api/' + n.route)}, fetch })\n`
2597
2597
  : '';
2598
- const createCall = isSql || isRest
2599
- ? `await ${src}.createRow(values)`
2600
- : `await ${n.sourceVar}.createRow({ [ID_FIELD]: nextId(${jsStr(n.idPrefix)}), ...values })`;
2601
- // RBAC only needs inline enforcement for the memory path; sql inherits it from the
2602
- // connected /api route (createKitHandlers authorize), reached via event.fetch.
2598
+ const createCall = `await ${src}.createRow(values)`;
2599
+ // RBAC only needs inline enforcement off the route (REST); a routed entity inherits
2600
+ // it from its /api route (createKitHandlers authorize), reached via event.fetch.
2603
2601
  const rbac = accessEnabled && !isSql;
2604
2602
  const localsArg = rbac ? ', locals' : '';
2605
2603
  const readGuard = rbac ? ` if (!authorizeAction(getServerRole({ locals }), 'read', SCREEN_IDS)) throw error(403, 'Not allowed')\n` : '';
@@ -2613,7 +2611,7 @@ import { planFromSearchParams } from '../lib/server/query'
2613
2611
  // This screen renders on the server: real SSR HTML, not just a server-side
2614
2612
  // load. Stated explicitly so it holds whichever way the root layout is set.
2615
2613
  export const ssr = true
2616
- ${idConst}${rbac ? `\nconst SCREEN_IDS = ${JSON.stringify(screenIds)}` : ''}
2614
+ ${rbac ? `\nconst SCREEN_IDS = ${JSON.stringify(screenIds)}` : ''}
2617
2615
  const FIELD_TYPES: Record<string, 'text' | 'number' | 'boolean'> = ${fieldTypesLit}
2618
2616
  ${srcHelper}
2619
2617
  /** Read a submitted form into a typed partial row. Booleans come from checkbox
@@ -2629,7 +2627,7 @@ function formToValues(fd: FormData): Record<string, unknown> {
2629
2627
  return values
2630
2628
  }
2631
2629
 
2632
- export const load: PageServerLoad = async ({ url${fetchArg}${localsArg} }) => {
2630
+ export const load: PageServerLoad = async ({ url${loadFetchArg}${localsArg} }) => {
2633
2631
  ${readGuard} const plan = planFromSearchParams(url, ${pageSize})
2634
2632
  const { rows, rowCount } = await ${src}.getRows(plan)
2635
2633
  ${relPrefetch ? relPrefetch + '\n' : ''} return { rows, total: rowCount, page: plan.pageIndex, size: plan.pageSize, sort: plan.sortModel${relReturn} }
@@ -2853,14 +2851,26 @@ ${g.title ? ` <legend>${htmlEsc(g.title)}</legend>\n` : ''}${inner}
2853
2851
  const q = sp.toString()
2854
2852
  void goto(q ? \`?\${q}\` : page.url.pathname, { keepFocus: true, noScroll: true })
2855
2853
  }
2854
+
2855
+ // Only a sort the URL does not already hold navigates. Grid releases before
2856
+ // the mount-report fix also reported the starting sort when they mounted,
2857
+ // and acting on that would drop ?page on every load.
2858
+ function onSort(s: Array<{ id: string; desc: boolean }>) {
2859
+ const sort = s.map((x) => \`\${x.id}:\${x.desc ? 'desc' : 'asc'}\`).join(',')
2860
+ if (sort !== (page.url.searchParams.get('sort') ?? '')) setParams({ sort: sort || null, page: null })
2861
+ }
2856
2862
  ${wantsFilter ? `
2857
2863
  // Filter via the URL (q = global search, f_<col> = per-column). The server's
2858
2864
  // planFromSearchParams reads these, so load() re-filters; reset to the first page.
2859
2865
  function applyFilters(f: { global: string; columns: Array<{ id: string; value: string }> }) {
2860
2866
  const sp = new URLSearchParams(page.url.searchParams)
2867
+ const filterParams = (p: URLSearchParams) => [...p].filter(([k]) => k === 'q' || k.startsWith('f_')).sort().join('&')
2868
+ const before = filterParams(sp)
2861
2869
  for (const k of [...sp.keys()]) if (k === 'q' || k.startsWith('f_')) sp.delete(k)
2862
2870
  if (f.global) sp.set('q', f.global)
2863
2871
  for (const c of f.columns) if (c.value) sp.set('f_' + c.id, c.value)
2872
+ // Nothing changed (older grid releases report the starting filters on mount).
2873
+ if (filterParams(sp) === before) return
2864
2874
  sp.delete('page')
2865
2875
  const q = sp.toString()
2866
2876
  void goto(q ? \`?\${q}\` : page.url.pathname, { keepFocus: true, noScroll: true })
@@ -2894,12 +2904,13 @@ ${facetForm}
2894
2904
  {columns}
2895
2905
  externalSort
2896
2906
  initialSorting={data.sort}
2907
+ showPagination
2897
2908
  externalPagination${wantsFilter ? '\n filterable\n externalFilter\n onFiltersChange={applyFilters}' : ''}
2898
2909
  rowCount={data.total}
2899
2910
  pageIndex={data.page}
2900
2911
  pageSize={data.size}
2901
- onSortingChange={(s) => setParams({ sort: s.map((x) => \`\${x.id}:\${x.desc ? 'desc' : 'asc'}\`).join(',') || null, page: null })}
2902
- onPaginationChange={(p) => setParams({ page: String(p.pageIndex), size: String(p.pageSize) })}
2912
+ onSortingChange={onSort}
2913
+ onPaginationChange={(p) => { if (p.pageIndex !== data.page || p.pageSize !== data.size) setParams({ page: String(p.pageIndex), size: String(p.pageSize) }) }}
2903
2914
  />
2904
2915
 
2905
2916
  {#if editing}
@@ -2962,8 +2973,12 @@ export function emitStudioProject(project) {
2962
2973
  const sources = Object.fromEntries(project.entities.map((e) => [e.name, entityDataSource(project, e.name)]));
2963
2974
  const accessEnabled = project.access?.enabled === true && (project.access?.roles.length ?? 0) > 0;
2964
2975
  const authEnabled = project.auth?.enabled === true;
2965
- // Audit only fires on server routes, so it needs at least one SQL-bound entity.
2966
- const auditEnabled = project.audit === true && Object.values(sources).some((s) => s.kind === 'sql');
2976
+ // SQL and in-memory entities each get an /api route: SQL because the database
2977
+ // is server-side, memory because its rows are held once on the server
2978
+ // ($lib/server/store) so server-rendered and browser-rendered screens share them.
2979
+ const routed = (entity) => sources[entity]?.kind === 'sql' || sources[entity]?.kind === 'memory';
2980
+ // Audit only fires on server routes, so it needs at least one routed entity.
2981
+ const auditEnabled = project.audit === true && project.entities.some((e) => routed(e.name));
2967
2982
  const i18nEnabled = project.i18n?.enabled === true && (project.i18n?.locales.length ?? 0) > 0;
2968
2983
  // Which screen(s) read-gate each entity's SQL route: a role may read the entity's
2969
2984
  // data only if it can open at least one screen bound to it (an entity with no
@@ -2980,7 +2995,7 @@ export function emitStudioProject(project) {
2980
2995
  const tenancyRequested = project.tenancy?.enabled === true;
2981
2996
  const tenancyOn = tenancyRequested && authEnabled && project.dataLayer === 'drizzle' && Object.values(sources).some((s) => s.kind === 'sql');
2982
2997
  const tenantCol = tenancyOn ? tenantField(project) : undefined;
2983
- const { files, prepared } = emitEntityModules(project.entities, { sources, accessEnabled, auditEnabled, screensByEntity, triggers: project.triggers, supabaseConn: project.supabase, supabaseAuth: project.auth?.enabled === true && project.auth.provider === 'supabase', tenantField: tenantCol, tenantScoped: (name) => isTenantScoped(project, name) });
2998
+ const { files, prepared } = emitEntityModules(project.entities, { sources, accessEnabled, auditEnabled, screensByEntity, triggers: project.triggers, supabaseConn: project.supabase, supabaseAuth: project.auth?.enabled === true && project.auth.provider === 'supabase', tenantField: tenantCol, tenantScoped: (name) => isTenantScoped(project, name), serverMemory: true });
2984
2999
  const byName = new Map(prepared.map((s) => [s.name, s]));
2985
3000
  // Raw (unprepared) entities keep their original field set - needed to derive
2986
3001
  // relation display-field names that match withRelationLabels (the prepared
@@ -3026,13 +3041,11 @@ export function emitStudioProject(project) {
3026
3041
  // driven sort/filter/page; a read-only screen (data-viz / detail) gets a
3027
3042
  // load-only server file, and the SAME page markup renders from data.*.
3028
3043
  if (isSsrScreen(project, screen)) {
3029
- const kind = sources[screen.entity]?.kind;
3030
- const srcKind = kind === 'sql' ? 'sql' : kind === 'rest' ? 'rest' : 'memory';
3031
3044
  if (ssrScreenShape(project, screen) === 'grid') {
3032
- pages.push(...emitSsrGridScreen(schema, screen, srcKind, accessEnabled, screensByEntity.get(screen.entity) ?? [], byName));
3045
+ pages.push(...emitSsrGridScreen(schema, screen, routed, accessEnabled, screensByEntity.get(screen.entity) ?? [], byName));
3033
3046
  }
3034
3047
  else {
3035
- pages.push(ssrReadServerFile(schema, screen, srcKind, accessEnabled, screensByEntity.get(screen.entity) ?? [], resolve), screenPage(schema, rawByName.get(screen.entity) ?? schema, screen, resolve, (name) => rawByName.get(name), accessEnabled, i18nEnabled, routeById, drillEnabled, true));
3048
+ pages.push(ssrReadServerFile(schema, screen, routed, accessEnabled, screensByEntity.get(screen.entity) ?? [], resolve), screenPage(schema, rawByName.get(screen.entity) ?? schema, screen, resolve, (name) => rawByName.get(name), accessEnabled, i18nEnabled, routeById, drillEnabled, true));
3036
3049
  }
3037
3050
  continue;
3038
3051
  }
@@ -3381,6 +3394,10 @@ export function requireTenant(event: TenantEvent): string {
3381
3394
  * route checks `CRON_SECRET` and runs the due handlers.
3382
3395
  */
3383
3396
  function jobsFiles(project, jobs, emailAvailable) {
3397
+ // A job runs on the server with no request, so an in-memory entity is read
3398
+ // from its server-held store directly rather than through its /api client.
3399
+ const isServerHeld = (ent) => entityDataSource(project, ent.name).kind === 'memory';
3400
+ const jobSourceVar = (ent) => (isServerHeld(ent) ? storeVar(ent) : namesFor(ent).sourceVar);
3384
3401
  const handler = (j) => {
3385
3402
  const label = jsStr(j.name);
3386
3403
  if (j.kind === 'email') {
@@ -3393,9 +3410,8 @@ function jobsFiles(project, jobs, emailAvailable) {
3393
3410
  const why = !emailAvailable ? 'email is not enabled on this project' : !j.to ? 'no recipient set' : `unknown entity ${j.entity}`;
3394
3411
  return ` ${JSON.stringify(j.id)}: async () => {\n // ${label}: cannot send - ${why}.\n console.warn('cron ${j.id}: skipped (${why})')\n },`;
3395
3412
  }
3396
- const n = namesFor(ent);
3397
3413
  return ` ${JSON.stringify(j.id)}: async () => {
3398
- const { rows, rowCount } = await ${n.sourceVar}.getRows({ startRow: 0, endRow: 10, sortModel: [], filterModel: {} })
3414
+ const { rows, rowCount } = await ${jobSourceVar(ent)}.getRows({ startRow: 0, endRow: 10, sortModel: [], filterModel: {} })
3399
3415
  const items = rows.map((r) => '<li>' + Object.values(r).slice(0, 3).map(String).join(' &middot; ') + '</li>').join('')
3400
3416
  await sendEmail(${to}, ${subject}, '<p>' + rowCount + ' ${ent.name} total. Most recent:</p><ul>' + items + '</ul>')
3401
3417
  },`;
@@ -3404,18 +3420,28 @@ function jobsFiles(project, jobs, emailAvailable) {
3404
3420
  return ` ${JSON.stringify(j.id)}: async () => {\n${body.split('\n').map((l) => ' ' + l).join('\n')}\n },`;
3405
3421
  };
3406
3422
  const entityImports = new Set();
3423
+ const storeImports = new Set();
3407
3424
  for (const j of jobs) {
3408
3425
  if (j.kind !== 'email')
3409
3426
  continue;
3410
3427
  const ent = project.entities.find((e) => e.name === j.entity);
3411
- if (ent && emailAvailable && j.to)
3412
- entityImports.add(namesFor(ent).sourceVar);
3428
+ if (!ent || !emailAvailable || !j.to)
3429
+ continue;
3430
+ if (isServerHeld(ent))
3431
+ storeImports.add(jobSourceVar(ent));
3432
+ else
3433
+ entityImports.add(jobSourceVar(ent));
3413
3434
  }
3414
3435
  const imports = [
3415
3436
  ...(entityImports.size ? [`import { ${[...entityImports].sort().join(', ')} } from '$lib/data'`] : []),
3437
+ ...(storeImports.size ? [`import { ${[...storeImports].sort().join(', ')} } from '$lib/server/store'`] : []),
3416
3438
  ...(jobs.some((j) => j.kind === 'email') && emailAvailable ? [`import { sendEmail } from '../lib/server/email'`] : []),
3417
3439
  ];
3418
- const table = jobs.map((j) => ` * ${j.id.padEnd(20)} ${j.cron.padEnd(16)} ${j.name}`).join('\n');
3440
+ // `//`, not ` * `: the header below is line comments, so a ` * ` row was not
3441
+ // a comment at all and every app with a scheduled job failed to compile.
3442
+ // No sample app has a job, which is how it went unnoticed - found by
3443
+ // `verify:app --all-paths`.
3444
+ const table = jobs.map((j) => `// ${j.id.padEnd(20)} ${j.cron.padEnd(16)} ${j.name}`).join('\n');
3419
3445
  const jobsTs = `// Regenerated by SvGrid Studio. Scheduled job handlers.
3420
3446
  //
3421
3447
  // Schedule (UTC):
@@ -4863,10 +4889,10 @@ function rootLayoutModule(project) {
4863
4889
  const anySsr = project.screens.some((s) => isSsrScreen(project, s));
4864
4890
  return {
4865
4891
  path: 'src/routes/+layout.ts',
4866
- description: anySsr ? 'Root layout options (SSR on; client-only screens opt out per page).' : 'Client SPA (in-memory sources persist across navigation).',
4892
+ description: anySsr ? 'Root layout options (SSR on; client-only screens opt out per page).' : 'Client SPA (no screen renders on the server).',
4867
4893
  contents: anySsr
4868
- ? `// Server rendering stays on (SvelteKit's default). Screens that have to run in\n// the browser - anything on an in-memory source, or a shape that can't render on\n// the server - opt out in their own +page.ts.\nexport const prerender = false\n`
4869
- : `// In-memory sources are module singletons, so render as a client SPA. Move an\n// entity to SQL / Supabase and its /api route still runs server-side.\nexport const ssr = false\nexport const prerender = false\n`,
4894
+ ? `// Server rendering stays on (SvelteKit's default). Screens that have to run in\n// the browser - a shape that can't render on the server, or a source the server\n// can't reach - opt out in their own +page.ts.\nexport const prerender = false\n`
4895
+ : `// No screen here renders on the server, so the app runs as a client SPA.\nexport const ssr = false\nexport const prerender = false\n`,
4870
4896
  };
4871
4897
  }
4872
4898
  /** The static SvelteKit + Vite scaffolding around the generated screens. */
@@ -5469,7 +5495,11 @@ export function emitStudioAppBundle(project) {
5469
5495
  ...(plan.deployWorkflow ? [{ path: '.github/workflows/deploy.yml', description: `Deploy to ${plan.label} on push to main.`, contents: plan.deployWorkflow }] : []),
5470
5496
  { path: 'DEPLOY.md', description: 'Deploy runbook (env vars, Git integration, CI, CLI).', contents: deployDocs(project, plan, envKeysUsed(allSource), generated.some((f) => f.path === 'drizzle.config.ts'), { hasDdl: generated.some((f) => f.path === 'db/schema.sql'), hasSeedScript: generated.some((f) => f.path === 'db/seed.ts') }) },
5471
5497
  ];
5472
- return [...scaffold, ...generated];
5498
+ const bundle = [...scaffold, ...generated];
5499
+ // SvelteKit 3 is an opt-in conversion of the finished bundle, so the default
5500
+ // SvelteKit 2 output above is untouched by it. See sveltekit3.ts for why the
5501
+ // default is not v3 yet (StackBlitz cannot run Vite 8).
5502
+ return project.kit === 3 ? toSvelteKit3(bundle) : bundle;
5473
5503
  }
5474
5504
  /**
5475
5505
  * Emit ONLY the app content (`src/routes` + `src/lib` + `db`) to drop into an
@@ -24,6 +24,8 @@ type Names = {
24
24
  export declare function namesFor(schema: EntitySchema): Names;
25
25
  /** Lookup variable name for a relation field, e.g. `postAuthorIdLookup`. */
26
26
  export declare const lookupVar: (schema: EntitySchema, field: string) => string;
27
+ /** Base (unwrapped) in-memory source variable, e.g. `userStore`. */
28
+ export declare const storeVar: (schema: EntitySchema) => string;
27
29
  type RelationInfo = {
28
30
  fkField: string;
29
31
  displayField: string;
@@ -94,6 +96,7 @@ export declare function emitEntityModules(schemas: EntitySchema[], opts?: {
94
96
  supabaseAuth?: boolean;
95
97
  tenantField?: string;
96
98
  tenantScoped?: (entity: string) => boolean;
99
+ serverMemory?: boolean;
97
100
  }): {
98
101
  files: GeneratedFile[];
99
102
  prepared: EntitySchema[];
@@ -81,7 +81,7 @@ function rowType(schema, derived) {
81
81
  return `export type ${pascal(schema.name)} = {\n${lines.join('\n')}\n}`;
82
82
  }
83
83
  /** Base (unwrapped) in-memory source variable, e.g. `userStore`. */
84
- const storeVar = (schema) => `${camel(schema.name)}Store`;
84
+ export const storeVar = (schema) => `${camel(schema.name)}Store`;
85
85
  /**
86
86
  * Map each relation FK field (whose target resolves) to the denormalized display
87
87
  * field name that `withRelationLabels` fills onto every row - e.g. `companyId` ->
@@ -336,39 +336,19 @@ const SQL_DRIVERS = {
336
336
  // DATABASE_AUTH_TOKEN the database token. Runs on serverless + edge.
337
337
  turso: { dep: '@libsql/client', imports: `import { createClient } from '@libsql/client'`, setup: `const client = createClient({ url: env.DATABASE_URL ?? '', authToken: env.DATABASE_AUTH_TOKEN })`, exec: `const rs = await client.execute({ sql: text, args: params })\n return rs.rows as unknown as Record<string, unknown>[]` },
338
338
  };
339
- /** A fully-connected SvelteKit API route for a SQL-bound entity. When RBAC is on,
340
- * the route imports the shared access policy and rejects unauthorized writes -
341
- * server-enforced, so a tampered client can't bypass it. When audit is on, every
342
- * successful write is recorded. */
343
339
  function sqlRouteFile(schema, table, dialect, feat = {}) {
344
340
  const n = namesFor(schema);
345
341
  const key = (dialect === 'supabase' ? 'postgres' : (dialect ?? 'postgres'));
346
342
  const driver = SQL_DRIVERS[key];
347
343
  const dialectLiteral = sqlDialectExpr(dialect);
348
- const accessImport = feat.access ? `\nimport { authorizeAction, getServerRole } from '$lib/access'` : '';
349
- const auditImport = feat.audit ? `\nimport { recordAudit } from '$lib/audit'` : '';
350
- const tenantImport = feat.tenantField ? `\nimport { requireTenant } from '$lib/server/tenant'` : '';
351
- // Every connected route validates writes against the schema server-side, and
352
- // (when enabled) authorizes them by role + records an audit entry.
353
- const opts = [`schema: ${n.schemaVar}`, `source`, `validate: true`];
354
- if (feat.access)
355
- opts.push(`// Server-enforced RBAC: the caller's role comes from the session (event.locals). Reads\n // are allowed only if the role can open one of this entity's own screens.\n authorize: ({ action, event }) => authorizeAction(getServerRole(event), action, ${JSON.stringify(feat.screenIds ?? [])})`);
356
- if (feat.tenantField)
357
- opts.push(`// Multi-tenancy, enforced server-side: reads are filtered to the caller's tenant,\n // creates are stamped with it, and update/delete re-read the target under the\n // scope first - so a guessed id cannot reach another tenant's row.\n scope: ({ event }) => ({ field: ${JSON.stringify(feat.tenantField)}, value: requireTenant(event) })`);
358
- if (feat.audit)
359
- opts.push(`// Record every successful write to the audit trail.\n audit: (e) => recordAudit({ entity: ${JSON.stringify(schema.name)}, action: e.action, recordId: e.id, values: e.values as Record<string, unknown> | undefined, actor: String(e.event.locals?.role ?? e.event.locals?.user ?? 'system') })`);
360
- // Server-enforced business rules: the entity's triggers compiled to lifecycle hooks.
361
- const hooks = triggerHooksOpt(feat.triggers);
362
- if (hooks)
363
- opts.push(`// Business rules, enforced server-side (a client that skips them still can't write bad data).\n ${hooks}`);
364
- const handlers = `export const { POST } = createKitHandlers({\n ${opts.join(',\n ')},\n})`;
344
+ const { imports, handlers } = kitHandlersBlock(schema, feat);
365
345
  return {
366
346
  path: `src/routes/api/${n.route}/+server.ts`,
367
347
  description: `Connected API route for ${n.label} (SQL via DATABASE_URL). Runs server-side.`,
368
348
  contents: `${driver.imports}
369
349
  import { env } from '$env/dynamic/private'
370
350
  import { createKitHandlers, createSqlDataSource } from '@svgrid/enterprise'
371
- import { ${n.schemaVar}, type ${n.type} } from '$lib/schemas'${accessImport}${auditImport}${tenantImport}
351
+ import { ${n.schemaVar}, type ${n.type} } from '$lib/schemas'${imports}
372
352
 
373
353
  ${driver.setup}
374
354
 
@@ -384,6 +364,104 @@ ${handlers}
384
364
  `,
385
365
  };
386
366
  }
367
+ /**
368
+ * The API route for an in-memory entity. Same handlers as a SQL route
369
+ * (validation, RBAC, triggers, audit), over the one server-held copy of the
370
+ * rows in `$lib/server/store`. Routing memory through the server like this is
371
+ * what lets its screens render on the server: before, the rows were a module
372
+ * singleton, so a server-rendered screen and a browser-rendered one each held
373
+ * their own copy and never saw each other's writes.
374
+ */
375
+ function memoryRouteFile(schema, feat = {}) {
376
+ const n = namesFor(schema);
377
+ const { imports, handlers } = kitHandlersBlock(schema, feat);
378
+ return {
379
+ path: `src/routes/api/${n.route}/+server.ts`,
380
+ description: `API route for ${n.label} (in-memory rows held on the server). Runs server-side.`,
381
+ contents: `import { createKitHandlers } from '@svgrid/enterprise'
382
+ import { ${n.schemaVar} } from '$lib/schemas'
383
+ import { ${storeVar(schema)} as source } from '$lib/server/store'${imports}
384
+
385
+ ${handlers}
386
+ `,
387
+ };
388
+ }
389
+ /**
390
+ * `$lib/server/store.ts`: the seeded rows of every in-memory entity, held once
391
+ * per server process. Only server code imports it (the API routes, scheduled
392
+ * jobs), so the seed data stays out of the browser bundle.
393
+ *
394
+ * The server owns new ids. A browser's `nextId()` counter only knows its own
395
+ * tab, so two tabs (or two people) would hand out the same id; a create whose
396
+ * id is missing or already taken gets the next free one here instead.
397
+ */
398
+ function serverStoreFile(entries) {
399
+ const schemaImports = entries.map((e) => namesFor(e.schema).schemaVar).join(', ');
400
+ const typeImports = entries.map((e) => `type ${namesFor(e.schema).type}`).join(', ');
401
+ const stores = entries
402
+ .map(({ schema, rows }) => {
403
+ const n = namesFor(schema);
404
+ const idField = resolveIdField(schema);
405
+ const numeric = schema.fields.find((f) => f.field === idField)?.type === 'number';
406
+ return `export const ${storeVar(schema)} = serverHeld<${n.type}>(${JSON.stringify(rows)}, ${n.schemaVar}, ${JSON.stringify(idField)}, ${numeric ? 'null' : JSON.stringify(n.idPrefix)})`;
407
+ })
408
+ .join('\n');
409
+ return {
410
+ path: 'src/lib/server/store.ts',
411
+ description: 'In-memory rows, held once on the server (seeded on start, reset on restart).',
412
+ contents: `/**
413
+ * The app's in-memory data, held once per server process and reached from the
414
+ * browser through each entity's /api route. Seeded on start, reset on restart:
415
+ * bind an entity to SQL in the Studio to keep its rows.
416
+ */
417
+ import { createInMemoryDataSource } from '@svgrid/enterprise'
418
+ import type { EntitySchema } from '@svgrid/enterprise'
419
+ import { ${schemaImports}, ${typeImports} } from '$lib/schemas'
420
+
421
+ /** An in-memory source that assigns a free id when a create brings none, or a
422
+ * taken one. \`prefix\` null means the id is numeric (max + 1). */
423
+ function serverHeld<T extends Record<string, unknown>>(rows: T[], schema: EntitySchema<T>, idField: string, prefix: string | null) {
424
+ const source = createInMemoryDataSource<T>(rows, schema)
425
+ let seq = rows.length
426
+ const freeId = (wanted: unknown): string | number => {
427
+ const taken = new Set(source.rows().map((r) => String(r[idField])))
428
+ if (wanted != null && wanted !== '' && !taken.has(String(wanted))) return wanted as string | number
429
+ if (prefix === null) return Math.max(0, ...source.rows().map((r) => Number(r[idField]) || 0)) + 1
430
+ let id = prefix + ++seq
431
+ while (taken.has(id)) id = prefix + ++seq
432
+ return id
433
+ }
434
+ return {
435
+ ...source,
436
+ createRow: (input: Partial<T>) => source.createRow({ ...input, [idField]: freeId(input[idField]) } as Partial<T>),
437
+ }
438
+ }
439
+
440
+ ${stores}
441
+ `,
442
+ };
443
+ }
444
+ /** The `createKitHandlers` call every entity route shares, plus the imports it needs. */
445
+ function kitHandlersBlock(schema, feat) {
446
+ const accessImport = feat.access ? `\nimport { authorizeAction, getServerRole } from '$lib/access'` : '';
447
+ const auditImport = feat.audit ? `\nimport { recordAudit } from '$lib/audit'` : '';
448
+ const tenantImport = feat.tenantField ? `\nimport { requireTenant } from '$lib/server/tenant'` : '';
449
+ // Every connected route validates writes against the schema server-side, and
450
+ // (when enabled) authorizes them by role + records an audit entry.
451
+ const opts = [`schema: ${namesFor(schema).schemaVar}`, `source`, `validate: true`];
452
+ if (feat.access)
453
+ opts.push(`// Server-enforced RBAC: the caller's role comes from the session (event.locals). Reads\n // are allowed only if the role can open one of this entity's own screens.\n authorize: ({ action, event }) => authorizeAction(getServerRole(event), action, ${JSON.stringify(feat.screenIds ?? [])})`);
454
+ if (feat.tenantField)
455
+ opts.push(`// Multi-tenancy, enforced server-side: reads are filtered to the caller's tenant,\n // creates are stamped with it, and update/delete re-read the target under the\n // scope first - so a guessed id cannot reach another tenant's row.\n scope: ({ event }) => ({ field: ${JSON.stringify(feat.tenantField)}, value: requireTenant(event) })`);
456
+ if (feat.audit)
457
+ opts.push(`// Record every successful write to the audit trail.\n audit: (e) => recordAudit({ entity: ${JSON.stringify(schema.name)}, action: e.action, recordId: e.id, values: e.values as Record<string, unknown> | undefined, actor: String(e.event.locals?.role ?? e.event.locals?.user ?? 'system') })`);
458
+ // Server-enforced business rules: the entity's triggers compiled to lifecycle hooks.
459
+ const hooks = triggerHooksOpt(feat.triggers);
460
+ if (hooks)
461
+ opts.push(`// Business rules, enforced server-side (a client that skips them still can't write bad data).\n ${hooks}`);
462
+ const handlers = `export const { POST } = createKitHandlers({\n ${opts.join(',\n ')},\n})`;
463
+ return { imports: `${accessImport}${auditImport}${tenantImport}`, handlers };
464
+ }
387
465
  /** Map an EntityField type to a Postgres column type for PGlite DDL. */
388
466
  const PG_COL_TYPE = { number: 'double precision', boolean: 'boolean', date: 'date', datetime: 'timestamptz', json: 'jsonb' };
389
467
  /** The shared PGlite client + table DDL + one-time seed, injected into data.ts. */
@@ -491,14 +569,14 @@ export function sqlDdlFiles(schemas, sources) {
491
569
  ...(inserts.length ? [{ path: 'db/seed.sql', description: 'Optional sample rows for the SQL entities.', contents: header('Sample seed rows (optional)') + inserts.join('\n\n') + '\n' }] : []),
492
570
  ];
493
571
  }
494
- function dataModule(entries, seed, sources, supabaseConn) {
572
+ function dataModule(entries, seed, sources, supabaseConn, serverMemory = false) {
495
573
  const schemas = entries.map((e) => e.schema);
496
574
  // Each type gets its own `type` keyword - SvelteKit enables verbatimModuleSyntax.
497
575
  const typeImports = schemas.map((s) => `type ${pascal(s.name)}`).join(', ');
498
576
  const schemaImports = schemas.map((s) => namesFor(s).schemaVar).join(', ');
499
577
  const hasRelations = entries.some((e) => e.infos.length > 0);
500
578
  const entImports = new Set();
501
- const needs = { supabase: false, sqlRoutes: [] };
579
+ const needs = { supabase: false, sqlRoutes: [], memoryRoutes: [] };
502
580
  // 1. Base store per entity, branched on its bound data source (default: seeded
503
581
  // in-memory, so the app runs with no backend).
504
582
  const stores = entries
@@ -537,11 +615,18 @@ function dataModule(entries, seed, sources, supabaseConn) {
537
615
  return `const ${store} = createSqlDataSource<${T}>({ schema: ${sv}, table: ${sq(src.table)}, dialect: { placeholders: '$', ilike: true }, execute: async (text, params) => { await pgReady; return (await pg.query(text, params)).rows as Record<string, unknown>[] } })`;
538
616
  }
539
617
  default: {
540
- entImports.add('createInMemoryDataSource');
541
618
  // Prefer curated seed on the source (a sample app), else the realistic generator.
542
619
  const curated = src.kind === 'memory' ? src.seed : undefined;
543
- const rows = JSON.stringify(curated ?? seed.get(e.schema.name) ?? []);
544
- return `const ${store} = createInMemoryDataSource<${T}>(${rows}, ${sv})`;
620
+ const rowList = curated ?? seed.get(e.schema.name) ?? [];
621
+ if (serverMemory) {
622
+ // The rows live on the server ($lib/server/store) and every screen,
623
+ // server-rendered or not, reaches them through /api/<entity>.
624
+ entImports.add('createKitDataSource');
625
+ needs.memoryRoutes.push({ schema: e.schema, rows: rowList });
626
+ return `const ${store} = createKitDataSource<${T}>({ endpoint: '/api/${namesFor(e.schema).route}' })`;
627
+ }
628
+ entImports.add('createInMemoryDataSource');
629
+ return `const ${store} = createInMemoryDataSource<${T}>(${JSON.stringify(rowList)}, ${sv})`;
545
630
  }
546
631
  }
547
632
  })
@@ -1067,7 +1152,7 @@ export function prepareEntities(schemas) {
1067
1152
  /** Emit the shared entity modules: `src/lib/schemas.ts` + `src/lib/data.ts` (+ `connections.ts`). */
1068
1153
  export function emitEntityModules(schemas, opts = {}) {
1069
1154
  const { entries, seed } = prepareEntities(schemas);
1070
- const { file: data, needs } = dataModule(entries, seed, opts.sources, opts.supabaseConn);
1155
+ const { file: data, needs } = dataModule(entries, seed, opts.sources, opts.supabaseConn, opts.serverMemory);
1071
1156
  const files = [schemasModule(entries), data];
1072
1157
  // Supabase Auth needs the shared client even with no Supabase-bound entity; seed
1073
1158
  // the .env paste hint from the project connection when nothing else did.
@@ -1087,6 +1172,20 @@ export function emitEntityModules(schemas, opts = {}) {
1087
1172
  // A shared (reference-data) entity stays global - no scope on its route.
1088
1173
  tenantField: opts.tenantField && opts.tenantScoped?.(r.schema.name) !== false ? opts.tenantField : undefined,
1089
1174
  }));
1175
+ if (needs.memoryRoutes.length) {
1176
+ files.push(serverStoreFile(needs.memoryRoutes));
1177
+ for (const r of needs.memoryRoutes)
1178
+ files.push(memoryRouteFile(r.schema, {
1179
+ access: opts.accessEnabled,
1180
+ audit: opts.auditEnabled,
1181
+ screenIds: opts.screensByEntity?.get(r.schema.name) ?? [],
1182
+ triggers: opts.triggers?.[r.schema.name],
1183
+ // The server copy is shared by every user, so a tenant-scoped app scopes
1184
+ // it too. Seeded rows carry no tenant, which hides them rather than
1185
+ // showing one tenant's writes to another.
1186
+ tenantField: opts.tenantField && opts.tenantScoped?.(r.schema.name) !== false ? opts.tenantField : undefined,
1187
+ }));
1188
+ }
1090
1189
  return { files, prepared: entries.map((e) => e.schema) };
1091
1190
  }
1092
1191
  /**
@@ -13,6 +13,7 @@ export { introspectDatabase, listDatabaseTables, countTableRows, probeConnection
13
13
  export { buildConnectionString, parseConnectionString, redactConnectionString, isFileDialect, DRIVER_PACKAGE, DEFAULT_PORT, type SqlConnectionParts, } from './db-connect-string.js';
14
14
  export { scaffold, mergeManaged, skipUserOwned, MANAGED_START, MANAGED_END, type GeneratedFile, type ScaffoldOptions, } from './scaffold.js';
15
15
  export { scaffoldApp, type ScaffoldAppOptions } from './scaffold-app.js';
16
+ export { toSvelteKit3, SVELTEKIT_3_DEV_DEPENDENCIES, SVELTEKIT_3_ADAPTERS, type KitVersion, } from './sveltekit3.js';
16
17
  export { emitStudioApp, emitEntityModules, entityScreenPage, prepareEntities } from './emit-schema.js';
17
18
  export { emitStudioProject, emitStudioAppBundle, emitStudioFragment, runtimeDeps, studioDeployInfo, screenHandles, screenDataset, ctxCompletions, componentHandleMembers, ctxAmbientDts, GRID_API_MEMBERS, type BlockHandle, type HandleTier } from './emit-project.js';
18
19
  export { sampleApps, getSampleApp, type SampleApp } from './samples/index.js';
@@ -25,7 +26,7 @@ export { crudSuiteScreens, addCrudSuite, crudAppFromSchemas, listScreen, formScr
25
26
  export { sanitizeStudioProject, buildStudioBugReport, type SanitizeResult, type BugReport, type BugReportInput, type StudioEnv, } from './bug-report.js';
26
27
  export { generateValue, generateRows } from './sample-data.js';
27
28
  export { studioThemes, defaultStudioTheme, getStudioTheme, resolveThemeTokens, resolveThemeTokensFor, isDarkTheme, themeStyleString, type StudioTheme } from './themes.js';
28
- export { createProject, defaultScreenFor, defaultBlockConfig, gridColumns, entityOf, blockPalette, addBlock, addBlockAt, addComponentBlock, removeBlock, duplicateBlock, moveBlock, reorderBlock, updateBlock, addEntity, removeEntity, updateEntity, formPlan, setEntityForm, setFormColumns, addFormSection, updateFormSection, removeFormSection, moveFormSection, moveFormField, moveFormFields, setFieldConditions, updateEntityField, setFieldInput, setFieldHidden, formControlsFor, formControlSettings, suggestFormSections, addScreen, addFreestandingScreen, addScreenAction, removeScreenAction, enableScreenCode, disableScreenCode, setScreenRenderGrid, screenLayoutOf, buildDockLayout, reconcileDock, syncDockPanes, setScreenLayout, isPaneLayout, PANE_LAYOUTS, buildCanvasLayout, canvasRectOf, setCanvasRect, CANVAS_COLS, CANVAS_ROW_PX, CANVAS_GAP_PX, type CanvasRect, applyGridPreset, gridPresetLabel, GRID_PRESETS, setScreenDock, reseedScreenDock, setDockPaneTitle, dockPaneTitleOf, dockPaneIds, setLayoutOpts, gridOpts, stackOpts, splitOpts, dockOpts, canvasOpts, type ScreenLayout, type GridPreset, type LayoutOpts, type GridLayoutOpts, type StackLayoutOpts, type SplitLayoutOpts, type DockLayoutOpts, type CanvasLayoutOpts, setHandlerBody, setScreenHandlersSource, setHandlerSteps, addHandlerStep, updateHandlerStep, moveHandlerStep, stepsToCode, compileStep, compileHandlerSteps, compileValue, compileCondition, compileTriggerStep, compileTriggerSteps, triggersOf, setTrigger, TRIGGER_EVENTS, type TriggerStep, type TriggerEvent, type EntityTriggers, setJob, type ScheduledJob, type ScheduledJobKind, setTenancy, tenantField, isTenantScoped, type TenancyConfig, clickSlot, ssrScreenShape, ssrEligible, withSsrDefaults, setScreenRenderMode, eventSlot, rowSelectSlot, changeSlot, FORM_SUBMIT, GRID_EVENTS, type GridEventDef, addStateVar, updateStateVar, removeStateVar, stateInitExpr, stateTsType, type ActionStep, type ActionStepType, type StateVar, type StateVarType, type LogicValue, type LogicOp, type Condition, type FieldValue, setComponentName, setComponentBinding, componentHasBindings, componentHandleName, ON_LOAD, removeScreen, updateScreen, duplicateScreen, reorderScreen, insertBlock, setDataSource, setDeployTarget, setAuth, setDataLayer, seedUsers, defaultEntitySource, flattenBlocks, TAB_CHILD_KINDS, CONTAINER_CHILD_KINDS, CONTAINER_DISPLAY_KINDS, addTab, removeTab, renameTab, addTabBlock, addTabComponent, removeTabBlock, addAccordionSection, removeAccordionSection, renameAccordionSection, setAccordionMultiple, addAccordionBlock, addAccordionComponent, removeAccordionBlock, setEntityDataSource, entityDataSource, setTheme, setThemePreset, setShell, sanitizeProject, screenFromTemplate, addScreenFromTemplate, serializeProject, parseProject, validateProject, isProjectValid, blockColumns, blockStyleCss, blockClassName, sanitizeClassName, mergeBlockStyle, type BlockStyle, roleCanScreen, roleCanAction, CRUD_ACTIONS, type CrudAction, type RoleAccess, type AccessControl, type AuthConfig, type OAuthProvider, type SeedUser, type I18nConfig, type DeployTarget, type StudioProject, type Screen, type ScreenNav, type Block, type BlockKind, type BlockConfig, type GridConfig, type GridColumnConfig, type ColumnFormat, type ColumnCellType, type GridExportConfig, type GridFilterUi, type TreeDataConfig, type SchedulerViewConfig, type SchedulerViewMode, type RowLink, type RowAction, type RowActionKind, type ActionConfig, type ComponentConfig, type ComponentBinding, type FormatOp, type FormatRule, type GridEditing, type GridDensity, type GridAlign, type PagerPosition, type FormConfig, type ChartConfig, type KpiConfig, type KpiFormat, type GaugeConfig, type TreeConfig, type TabsConfig, type StudioTab, type AccordionConfig, type AccordionSection, type DashboardConfig, type MasterDetailConfig, type LookupConfig, type PivotConfig, type FilterPanelConfig, type RecordConfig, type BoardConfig, type CalendarConfig, type DetailConfig, type DetailRelated, type Reduce, type DataSourceKind, type EntityDataSource, type MemorySource, type RestSource, type RestAdapterConfig, type SqlSource, type SupabaseSource, type PgliteSource, type RestMethod, type ParamLocation, type ParamType, type RequestParam, type SqlDialectKind, type ProjectTheme, type ShellConfig, type ShellStyle, type ScreenTemplate, type PaletteItem, type ProjectIssue, } from './project.js';
29
+ export { createProject, defaultScreenFor, defaultBlockConfig, gridColumns, entityOf, blockPalette, addBlock, addBlockAt, addComponentBlock, removeBlock, duplicateBlock, moveBlock, reorderBlock, updateBlock, addEntity, removeEntity, updateEntity, formPlan, setEntityForm, setFormColumns, addFormSection, updateFormSection, removeFormSection, moveFormSection, moveFormField, moveFormFields, setFieldConditions, updateEntityField, setFieldInput, setFieldHidden, formControlsFor, formControlSettings, suggestFormSections, addScreen, addFreestandingScreen, addScreenAction, removeScreenAction, enableScreenCode, disableScreenCode, setScreenRenderGrid, screenLayoutOf, buildDockLayout, reconcileDock, syncDockPanes, setScreenLayout, isPaneLayout, PANE_LAYOUTS, buildCanvasLayout, canvasRectOf, setCanvasRect, CANVAS_COLS, CANVAS_ROW_PX, CANVAS_GAP_PX, type CanvasRect, applyGridPreset, gridPresetLabel, GRID_PRESETS, setScreenDock, reseedScreenDock, setDockPaneTitle, dockPaneTitleOf, dockPaneIds, setLayoutOpts, gridOpts, stackOpts, splitOpts, dockOpts, canvasOpts, type ScreenLayout, type GridPreset, type LayoutOpts, type GridLayoutOpts, type StackLayoutOpts, type SplitLayoutOpts, type DockLayoutOpts, type CanvasLayoutOpts, setHandlerBody, setScreenHandlersSource, setHandlerSteps, addHandlerStep, updateHandlerStep, moveHandlerStep, stepsToCode, compileStep, compileHandlerSteps, compileValue, compileCondition, compileTriggerStep, compileTriggerSteps, triggersOf, setTrigger, TRIGGER_EVENTS, type TriggerStep, type TriggerEvent, type EntityTriggers, setJob, type ScheduledJob, type ScheduledJobKind, setTenancy, tenantField, isTenantScoped, type TenancyConfig, clickSlot, ssrScreenShape, ssrEligible, withSsrDefaults, setScreenRenderMode, eventSlot, rowSelectSlot, changeSlot, FORM_SUBMIT, GRID_EVENTS, type GridEventDef, addStateVar, updateStateVar, removeStateVar, stateInitExpr, stateTsType, type ActionStep, type ActionStepType, type StateVar, type StateVarType, type LogicValue, type LogicOp, type Condition, type FieldValue, setComponentName, setComponentBinding, componentHasBindings, componentHandleName, ON_LOAD, removeScreen, updateScreen, duplicateScreen, reorderScreen, insertBlock, setDataSource, setDeployTarget, setKitVersion, setAuth, setDataLayer, seedUsers, defaultEntitySource, flattenBlocks, TAB_CHILD_KINDS, CONTAINER_CHILD_KINDS, CONTAINER_DISPLAY_KINDS, addTab, removeTab, renameTab, addTabBlock, addTabComponent, removeTabBlock, addAccordionSection, removeAccordionSection, renameAccordionSection, setAccordionMultiple, addAccordionBlock, addAccordionComponent, removeAccordionBlock, setEntityDataSource, entityDataSource, setTheme, setThemePreset, setShell, sanitizeProject, screenFromTemplate, addScreenFromTemplate, serializeProject, parseProject, validateProject, isProjectValid, blockColumns, blockStyleCss, blockClassName, sanitizeClassName, mergeBlockStyle, type BlockStyle, roleCanScreen, roleCanAction, CRUD_ACTIONS, type CrudAction, type RoleAccess, type AccessControl, type AuthConfig, type OAuthProvider, type SeedUser, type I18nConfig, type DeployTarget, type StudioProject, type Screen, type ScreenNav, type Block, type BlockKind, type BlockConfig, type GridConfig, type GridColumnConfig, type ColumnFormat, type ColumnCellType, type GridExportConfig, type GridFilterUi, type TreeDataConfig, type SchedulerViewConfig, type SchedulerViewMode, type RowLink, type RowAction, type RowActionKind, type ActionConfig, type ComponentConfig, type ComponentBinding, type FormatOp, type FormatRule, type GridEditing, type GridDensity, type GridAlign, type PagerPosition, type FormConfig, type ChartConfig, type KpiConfig, type KpiFormat, type GaugeConfig, type TreeConfig, type TabsConfig, type StudioTab, type AccordionConfig, type AccordionSection, type DashboardConfig, type MasterDetailConfig, type LookupConfig, type PivotConfig, type FilterPanelConfig, type RecordConfig, type BoardConfig, type CalendarConfig, type DetailConfig, type DetailRelated, type Reduce, type DataSourceKind, type EntityDataSource, type MemorySource, type RestSource, type RestAdapterConfig, type SqlSource, type SupabaseSource, type PgliteSource, type RestMethod, type ParamLocation, type ParamType, type RequestParam, type SqlDialectKind, type ProjectTheme, type ShellConfig, type ShellStyle, type ScreenTemplate, type PaletteItem, type ProjectIssue, } from './project.js';
29
30
  export { verifyScaffold, summarizeVerify, type VerifyResult, type VerifyIssue, type VerifySeverity, } from './verify.js';
30
31
  export { UI_COMPONENT_REGISTRY, uiComponentSpec, gridPropSurface, gridApiSettableProps, gridIconGroups, GRID_CURATED_PROPS, STANDARD_UI_EVENTS, type UiPropType, type UiPropGroup, type UiComponentEvent, type UiComponentProp, type UiComponentSpec, } from './ui-components.js';
31
32
  export { buildCopilotMessages, projectFromModelText, type CopilotMessages } from './copilot-core.js';