@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
@@ -2027,6 +2027,181 @@ function scaffoldApp(schemas, options = {}) {
2027
2027
  return { files };
2028
2028
  }
2029
2029
  //#endregion
2030
+ //#region src/studio/sveltekit3.ts
2031
+ /** Toolchain SvelteKit 3 requires. Minimums are from the v3 migration guide. */
2032
+ var SVELTEKIT_3_DEV_DEPENDENCIES = {
2033
+ "@sveltejs/kit": "^3.0.0",
2034
+ "@sveltejs/vite-plugin-svelte": "^7.0.0",
2035
+ svelte: "^5.57.1",
2036
+ "svelte-check": "^4.7.5",
2037
+ typescript: "^6.0.0",
2038
+ vite: "^8.0.12"
2039
+ };
2040
+ /**
2041
+ * The first major of each adapter whose peerDependency is `@sveltejs/kit ^3`,
2042
+ * read from npm on 2026-10-08. None of the v3 adapter behaviour changes (node's
2043
+ * ORIGIN, vercel's edge runtime, cloudflare's platform APIs) reach Studio
2044
+ * output, so this is a version bump only.
2045
+ */
2046
+ var SVELTEKIT_3_ADAPTERS = {
2047
+ "@sveltejs/adapter-auto": "^8.0.0",
2048
+ "@sveltejs/adapter-node": "^6.0.0",
2049
+ "@sveltejs/adapter-vercel": "^7.0.0",
2050
+ "@sveltejs/adapter-netlify": "^7.0.1",
2051
+ "@sveltejs/adapter-cloudflare": "^8.0.0"
2052
+ };
2053
+ var SOURCE = /\.(ts|js|svelte)$/;
2054
+ function toSvelteKit3(files) {
2055
+ const paths = new Set(files.map((f) => f.path));
2056
+ const adapter = adapterOf(files);
2057
+ const envNames = /* @__PURE__ */ new Set();
2058
+ const out = [];
2059
+ for (const file of files) {
2060
+ if (file.path === "svelte.config.js") continue;
2061
+ if (file.path === "package.json") {
2062
+ out.push({
2063
+ ...file,
2064
+ contents: packageJson3(file.contents)
2065
+ });
2066
+ continue;
2067
+ }
2068
+ if (file.path === "vite.config.ts") {
2069
+ out.push({
2070
+ ...file,
2071
+ description: `Vite config, including SvelteKit (${adapter}).`,
2072
+ contents: viteConfig3(file.contents, adapter)
2073
+ });
2074
+ continue;
2075
+ }
2076
+ if (file.path === "tsconfig.json") {
2077
+ out.push({
2078
+ ...file,
2079
+ contents: TSCONFIG_3
2080
+ });
2081
+ continue;
2082
+ }
2083
+ if (SOURCE.test(file.path) && file.path !== "drizzle.config.ts") {
2084
+ const contents = rewriteSource(file.contents, paths, envNames);
2085
+ out.push(contents === file.contents ? file : {
2086
+ ...file,
2087
+ contents
2088
+ });
2089
+ continue;
2090
+ }
2091
+ out.push(file);
2092
+ }
2093
+ if (envNames.size) out.push(envModule([...envNames].sort()));
2094
+ return out;
2095
+ }
2096
+ function adapterOf(files) {
2097
+ const pkg = files.find((f) => f.path === "package.json");
2098
+ if (!pkg) return "@sveltejs/adapter-auto";
2099
+ const dev = JSON.parse(pkg.contents).devDependencies ?? {};
2100
+ return Object.keys(dev).find((k) => k.startsWith("@sveltejs/adapter-")) ?? "@sveltejs/adapter-auto";
2101
+ }
2102
+ function packageJson3(contents) {
2103
+ const pkg = JSON.parse(contents);
2104
+ const dev = { ...pkg.devDependencies ?? {} };
2105
+ for (const [name, range] of Object.entries(SVELTEKIT_3_DEV_DEPENDENCIES)) dev[name] = range;
2106
+ for (const name of Object.keys(dev)) if (SVELTEKIT_3_ADAPTERS[name]) dev[name] = SVELTEKIT_3_ADAPTERS[name];
2107
+ pkg.devDependencies = dev;
2108
+ pkg.imports = {
2109
+ "#lib": "./src/lib/index.js",
2110
+ "#lib/*": "./src/lib/*"
2111
+ };
2112
+ return JSON.stringify(pkg, null, 2) + "\n";
2113
+ }
2114
+ /**
2115
+ * The v2 vite.config.ts with SvelteKit's config folded into the plugin call.
2116
+ * Everything else in the file (the dev-server watch exclusions) is kept.
2117
+ */
2118
+ function viteConfig3(v2, adapter) {
2119
+ const imports = `import adapter from '${adapter}'\nimport { vitePreprocess } from '@sveltejs/vite-plugin-svelte'\n`;
2120
+ const withPlugin = v2.replace("sveltekit()", "sveltekit({ preprocess: vitePreprocess(), adapter: adapter() })");
2121
+ if (withPlugin === v2) throw new Error("toSvelteKit3: vite.config.ts has no sveltekit() call to configure");
2122
+ return imports + withPlugin;
2123
+ }
2124
+ var TSCONFIG_3 = `{
2125
+ "extends": "$app/tsconfig",
2126
+ "compilerOptions": {
2127
+ "sourceMap": true,
2128
+ "strict": true
2129
+ },
2130
+ "include": ["src", "vite.config.ts"]
2131
+ }
2132
+ `;
2133
+ function rewriteSource(src, paths, envNames) {
2134
+ let s = src;
2135
+ s = rewriteLibImports(s, paths);
2136
+ s = rewriteEnv(s, envNames);
2137
+ s = rewriteKitImports(s);
2138
+ s = s.replace(/\{\s*keepFocus:\s*true,\s*noScroll:\s*true\s*\}/g, "{ reset: false }");
2139
+ s = s.replace(/new URLSearchParams\(page\.url\.searchParams\)/g, "new URLSearchParams(page.url.search)");
2140
+ return s;
2141
+ }
2142
+ /**
2143
+ * `$lib/x` -> `#lib/x.js`. Subpath imports need a real extension, and the right
2144
+ * one depends on what the file actually is, so each specifier is resolved
2145
+ * against the generated paths rather than guessed: `x.ts` and `x.svelte.ts`
2146
+ * are imported as `.js` (TypeScript maps it back), anything already carrying
2147
+ * its extension (`.svelte`, `.js`) is left as it is, and a directory resolves
2148
+ * to its index.
2149
+ */
2150
+ function rewriteLibImports(src, paths) {
2151
+ return src.replace(/(['"`])\$lib\/([^'"`]+)\1/g, (whole, q, spec) => {
2152
+ const base = `src/lib/${spec}`;
2153
+ if (paths.has(base)) return `${q}#lib/${spec}${q}`;
2154
+ if (paths.has(`${base}.ts`) || paths.has(`${base}.js`)) return `${q}#lib/${spec}.js${q}`;
2155
+ if (paths.has(`${base}/index.ts`) || paths.has(`${base}/index.js`)) return `${q}#lib/${spec}/index.js${q}`;
2156
+ return `${q}#lib/${spec}.js${q}`;
2157
+ });
2158
+ }
2159
+ /**
2160
+ * `import { env } from '$env/dynamic/private'` + `env.X`
2161
+ * -> `import { X } from '$app/env/private'` + `X`
2162
+ * Every name read is collected so src/env.ts can declare it, which v3 requires.
2163
+ */
2164
+ function rewriteEnv(src, envNames) {
2165
+ if (!/from\s+['"]\$env\/dynamic\/private['"]/.test(src)) return src;
2166
+ const used = /* @__PURE__ */ new Set();
2167
+ const body = src.replace(/(?<![.\w])env\.([A-Z_][A-Z0-9_]*)/g, (_m, name) => {
2168
+ used.add(name);
2169
+ return name;
2170
+ });
2171
+ for (const n of used) envNames.add(n);
2172
+ const names = [...used].sort();
2173
+ return body.replace(/import\s*\{\s*env\s*\}\s*from\s*(['"])\$env\/dynamic\/private\1/, names.length ? `import { ${names.join(", ")} } from '$app/env/private'` : "");
2174
+ }
2175
+ /**
2176
+ * Types that moved out of `@sveltejs/kit`, and the deprecated `json` helper.
2177
+ * `fail`, `redirect` and `error` stay where they are.
2178
+ */
2179
+ function rewriteKitImports(src) {
2180
+ let s = src;
2181
+ s = s.replace(/import\s+type\s*\{\s*Handle\s*\}\s*from\s*(['"])@sveltejs\/kit\1/g, `import type { Handle } from '@sveltejs/kit/hooks'`);
2182
+ s = s.replace(/import\s+type\s*\{\s*SubmitFunction\s*\}\s*from\s*(['"])@sveltejs\/kit\1/g, `import type { SubmitFunction } from '$app/forms'`);
2183
+ const kitImport = /import\s*\{([^}]*)\}\s*from\s*(['"])@sveltejs\/kit\2/;
2184
+ const names = (s.match(kitImport)?.[1] ?? "").split(",").map((x) => x.trim());
2185
+ if (names.includes("json")) {
2186
+ s = s.replace(/(?<![.\w])json\(/g, "Response.json(");
2187
+ const rest = names.filter((x) => x && x !== "json");
2188
+ s = s.replace(kitImport, rest.length ? `import { ${rest.join(", ")} } from '@sveltejs/kit'` : "");
2189
+ }
2190
+ return s;
2191
+ }
2192
+ function envModule(names) {
2193
+ return {
2194
+ path: "src/env.ts",
2195
+ description: "Environment variables the app reads (SvelteKit 3 requires declaring them).",
2196
+ contents: `import { defineEnvVars } from '@sveltejs/kit/env'
2197
+
2198
+ // SvelteKit 3 reads only the variables declared here. A missing one comes
2199
+ // through as '' - every read in this app already falls back on that
2200
+ // (a dev default, '?? ""', or an if), so that is the intended behaviour.
2201
+ export const variables = defineEnvVars({\n${names.map((n) => ` ${n}: { schema: (input) => input ?? '' },`).join("\n")}\n})\n`
2202
+ };
2203
+ }
2204
+ //#endregion
2030
2205
  //#region src/expressions/parse.ts
2031
2206
  /** Every column id referenced anywhere in a predicate (deduped, in order). */
2032
2207
  function collectColumnRefs(expr) {
@@ -8888,14 +9063,14 @@ var GENERATED_UI_SURFACE = {
8888
9063
  "key": "columnVirtualization",
8889
9064
  "label": "Column Virtualization",
8890
9065
  "type": "boolean",
8891
- "description": "Render only the columns in view, the horizontal counterpart of `virtualization`. **On by default**, which is what makes a 100-column grid scroll smoothly. It recycles column DOM nodes, so it cannot coexist with sticky pinned columns - turn it off if you need pinning to survive horizontal scrolling.",
9066
+ "description": "Render only the columns in view, the horizontal counterpart of `virtualization`. **On by default**, which is what makes a 100-column grid scroll smoothly. Pinned columns stay rendered while it is on: the pinned-left and pinned-right columns are drawn as their own runs either side of the virtual window, so a wide grid with pinned edges still renders only the columns in view plus the pinned ones.",
8892
9067
  "group": "common"
8893
9068
  },
8894
9069
  {
8895
9070
  "key": "columnOverscan",
8896
9071
  "label": "Column Overscan",
8897
9072
  "type": "number",
8898
- "description": "Extra columns rendered either side of the viewport when `columnVirtualization` is on. Defaults to 3.",
9073
+ "description": "Columns kept rendered beyond the viewport when `columnVirtualization` is on: this many ahead of a horizontal scroll at all times, one behind it. The rendered columns move three at a time, so right after a move up to `columnOverscan + 2` are rendered ahead. Defaults to 3.",
8899
9074
  "group": "common"
8900
9075
  },
8901
9076
  {
@@ -8909,7 +9084,7 @@ var GENERATED_UI_SURFACE = {
8909
9084
  "key": "initialColumnPinning",
8910
9085
  "label": "Initial Column Pinning",
8911
9086
  "type": "json",
8912
- "description": "Columns pinned to the left/right edge on mount. Each entry is a column id (matches `ColumnDef.field` when no explicit id is set). The internal pinning state is seeded once at mount; user-driven pinning via the column menu still works and overrides this default. Requires `columnVirtualization={false}` to be visible in the menu UI (sticky positioning can't co-exist with column virtualization since the virtualizer recycles DOM nodes).",
9087
+ "description": "Columns pinned to the left/right edge on mount. Each entry is a column id (matches `ColumnDef.field` when no explicit id is set). The internal pinning state is seeded once at mount; user-driven pinning via the column menu still works and overrides this default. Works with column virtualization on (the default).",
8913
9088
  "group": "common"
8914
9089
  },
8915
9090
  {
@@ -9187,7 +9362,7 @@ var GENERATED_UI_SURFACE = {
9187
9362
  "key": "conditionalFormats",
9188
9363
  "label": "Conditional Formats",
9189
9364
  "type": "json",
9190
- "description": "Excel-style conditional formatting. A list of value-driven rules that color cells: `colorScale` (gradient across the column range), `dataBar` (in-cell proportional bar), `iconSet` (arrows / traffic / triangles by threshold), and `rule` (apply a style when a predicate matches). Scope a format to specific columns with `columns: [...]`, or omit it to apply to every column. Later entries win on conflict.",
9365
+ "description": "Excel-style conditional formatting. A list of value-driven rules that color cells: `colorScale` (gradient across the column range), `dataBar` (in-cell proportional bar), `iconSet` (arrows / traffic / triangles by threshold), and `rule` (apply a style when a predicate matches). Scope a format to specific columns with `columns: [...]`, or omit it to apply to every column. Later entries win on conflict. `colorScale` and `dataBar` apply to data rows only: group, footer and grand-total rows carry subtotals on a different scale, so they keep the `rule` and `iconSet` formats and drop the scaled ones. On a server row model, give scaled formats `minValue` / `maxValue`; the grid does not scan rows it has not loaded for a range.",
9191
9366
  "group": "common"
9192
9367
  },
9193
9368
  {
@@ -9311,6 +9486,14 @@ var GENERATED_UI_SURFACE = {
9311
9486
  "description": "Merged cells: rectangles drawn as one cell, a spreadsheet's Merge & Center. Each entry is the top-left cell (display indices) and how many rows and columns it covers. The origin's td takes the span and shows the origin's value; the covered cells are not drawn, a selection grows to whole merges, the active cell inside a merge is its origin, and the arrow keys step over a merge as one cell. A merge that crosses the frozen boundary or the rendered window is drawn in parts, one per band. The grid does not write into covered cells on its own; a consumer that merges cells keeps them empty or marks them read-only through the column's `editable`.",
9312
9487
  "group": "common"
9313
9488
  },
9489
+ {
9490
+ "key": "rowLayout",
9491
+ "label": "Row Layout",
9492
+ "type": "select",
9493
+ "options": ["flex", "table"],
9494
+ "description": "How rows are laid out. `'flex'` (the default) draws each row as a line of fixed-width cells, so adding or removing cells in one row - what a horizontal scroll does every frame - lays out that row only. `'table'` uses the HTML table algorithm, which re-lays every row on such a change but can draw a merged cell spanning rows; the grid switches to it on its own when `mergedCells` has one.",
9495
+ "group": "common"
9496
+ },
9314
9497
  {
9315
9498
  "key": "pinnedBottomRows",
9316
9499
  "label": "Pinned Bottom Rows",
@@ -12115,13 +12298,14 @@ function ssrScreenShape(project, screen) {
12115
12298
  function ssrEligible(project, screen) {
12116
12299
  return ssrScreenShape(project, screen) !== null;
12117
12300
  }
12118
- /** Set a screen's render mode ('spa' clears back to the default). */
12301
+ /** Set a screen's render mode. Both modes are stored: a screen switched to
12302
+ * 'spa' stays client-rendered even where `withSsrDefaults` would turn it on. */
12119
12303
  function setScreenRenderMode(project, screenId, mode) {
12120
12304
  return {
12121
12305
  ...project,
12122
12306
  screens: project.screens.map((s) => s.id === screenId ? {
12123
12307
  ...s,
12124
- renderMode: mode === "ssr" ? "ssr" : void 0
12308
+ renderMode: mode
12125
12309
  } : s)
12126
12310
  };
12127
12311
  }
@@ -12130,19 +12314,23 @@ function isSsrScreen(project, screen) {
12130
12314
  return screenRenderMode(project, screen) === "ssr" && ssrEligible(project, screen);
12131
12315
  }
12132
12316
  /**
12133
- * Turn server rendering on for every screen that can support it, for entities
12134
- * backed by a real database. Applied when a project is first generated, so a new
12135
- * app reads as idiomatic SvelteKit (a `load` and form `actions`) rather than a
12136
- * client SPA. Screens that already state a mode are left alone, so this never
12137
- * overrides a choice somebody made.
12317
+ * Turn server rendering on for every screen that can support it. Applied when a
12318
+ * project is first generated, so a new app reads as idiomatic SvelteKit (a
12319
+ * `load` and form `actions`) rather than a client SPA. Screens that already
12320
+ * state a mode are left alone, so this never overrides a choice somebody made.
12138
12321
  *
12139
- * Deliberately limited to sources that hold one copy of the data. In-memory and
12140
- * PGlite sources are module singletons, so an SSR screen would read and write the
12141
- * *server's* copy of the rows while the app's remaining SPA screens read the
12142
- * browser's - create a row on one and the other never sees it. SQL and REST have
12143
- * no such split: every path goes to the same database or the same remote API.
12322
+ * Limited to sources that hold one copy of the data. SQL and REST always did:
12323
+ * every path goes to the same database or the same remote API. In-memory rows
12324
+ * are held once on the server ($lib/server/store) and every screen reaches them
12325
+ * through the entity's /api route, so an SSR screen and an SPA screen see the
12326
+ * same rows. PGlite lives in the browser's IndexedDB, which the server cannot
12327
+ * read, so it stays client-rendered.
12144
12328
  */
12145
- var SSR_DEFAULT_KINDS = /* @__PURE__ */ new Set(["sql", "rest"]);
12329
+ var SSR_DEFAULT_KINDS = /* @__PURE__ */ new Set([
12330
+ "sql",
12331
+ "rest",
12332
+ "memory"
12333
+ ]);
12146
12334
  function withSsrDefaults(project) {
12147
12335
  return {
12148
12336
  ...project,
@@ -14170,6 +14358,18 @@ function setDataSource(project, dataSource) {
14170
14358
  };
14171
14359
  }
14172
14360
  /** Set the deploy target (SvelteKit adapter + provider config the bundle emits). */
14361
+ /** Target a SvelteKit major. 2 is the default and is stored as the field's
14362
+ * absence, the same way `deploy: 'auto'` is. */
14363
+ function setKitVersion(project, kit) {
14364
+ if (kit === 2) {
14365
+ const { kit: _drop, ...rest } = project;
14366
+ return rest;
14367
+ }
14368
+ return {
14369
+ ...project,
14370
+ kit: 3
14371
+ };
14372
+ }
14173
14373
  function setDeployTarget(project, deploy) {
14174
14374
  if (deploy === "auto") {
14175
14375
  const { deploy: _drop, ...rest } = project;
@@ -14458,6 +14658,7 @@ function parseProject(json) {
14458
14658
  ...typeof p.audit === "boolean" ? { audit: p.audit } : {},
14459
14659
  ...p.i18n && typeof p.i18n === "object" ? { i18n: p.i18n } : {},
14460
14660
  ...typeof p.deploy === "string" && p.deploy !== "auto" ? { deploy: p.deploy } : {},
14661
+ ...p.kit === 3 ? { kit: 3 } : {},
14461
14662
  ...p.triggers && typeof p.triggers === "object" ? { triggers: p.triggers } : {}
14462
14663
  });
14463
14664
  }
@@ -15287,35 +15488,18 @@ var SQL_DRIVERS = {
15287
15488
  exec: `const rs = await client.execute({ sql: text, args: params })\n return rs.rows as unknown as Record<string, unknown>[]`
15288
15489
  }
15289
15490
  };
15290
- /** A fully-connected SvelteKit API route for a SQL-bound entity. When RBAC is on,
15291
- * the route imports the shared access policy and rejects unauthorized writes -
15292
- * server-enforced, so a tampered client can't bypass it. When audit is on, every
15293
- * successful write is recorded. */
15294
15491
  function sqlRouteFile(schema, table, dialect, feat = {}) {
15295
15492
  const n = namesFor(schema);
15296
15493
  const driver = SQL_DRIVERS[dialect === "supabase" ? "postgres" : dialect ?? "postgres"];
15297
15494
  const dialectLiteral = sqlDialectExpr(dialect);
15298
- const accessImport = feat.access ? `\nimport { authorizeAction, getServerRole } from '$lib/access'` : "";
15299
- const auditImport = feat.audit ? `\nimport { recordAudit } from '$lib/audit'` : "";
15300
- const tenantImport = feat.tenantField ? `\nimport { requireTenant } from '$lib/server/tenant'` : "";
15301
- const opts = [
15302
- `schema: ${n.schemaVar}`,
15303
- `source`,
15304
- `validate: true`
15305
- ];
15306
- if (feat.access) 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 ?? [])})`);
15307
- if (feat.tenantField) 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) })`);
15308
- if (feat.audit) 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') })`);
15309
- const hooks = triggerHooksOpt(feat.triggers);
15310
- if (hooks) opts.push(`// Business rules, enforced server-side (a client that skips them still can't write bad data).\n ${hooks}`);
15311
- const handlers = `export const { POST } = createKitHandlers({\n ${opts.join(",\n ")},\n})`;
15495
+ const { imports, handlers } = kitHandlersBlock(schema, feat);
15312
15496
  return {
15313
15497
  path: `src/routes/api/${n.route}/+server.ts`,
15314
15498
  description: `Connected API route for ${n.label} (SQL via DATABASE_URL). Runs server-side.`,
15315
15499
  contents: `${driver.imports}
15316
15500
  import { env } from '$env/dynamic/private'
15317
15501
  import { createKitHandlers, createSqlDataSource } from '@svgrid/enterprise'
15318
- import { ${n.schemaVar}, type ${n.type} } from '$lib/schemas'${accessImport}${auditImport}${tenantImport}
15502
+ import { ${n.schemaVar}, type ${n.type} } from '$lib/schemas'${imports}
15319
15503
 
15320
15504
  ${driver.setup}
15321
15505
 
@@ -15331,6 +15515,99 @@ ${handlers}
15331
15515
  `
15332
15516
  };
15333
15517
  }
15518
+ /**
15519
+ * The API route for an in-memory entity. Same handlers as a SQL route
15520
+ * (validation, RBAC, triggers, audit), over the one server-held copy of the
15521
+ * rows in `$lib/server/store`. Routing memory through the server like this is
15522
+ * what lets its screens render on the server: before, the rows were a module
15523
+ * singleton, so a server-rendered screen and a browser-rendered one each held
15524
+ * their own copy and never saw each other's writes.
15525
+ */
15526
+ function memoryRouteFile(schema, feat = {}) {
15527
+ const n = namesFor(schema);
15528
+ const { imports, handlers } = kitHandlersBlock(schema, feat);
15529
+ return {
15530
+ path: `src/routes/api/${n.route}/+server.ts`,
15531
+ description: `API route for ${n.label} (in-memory rows held on the server). Runs server-side.`,
15532
+ contents: `import { createKitHandlers } from '@svgrid/enterprise'
15533
+ import { ${n.schemaVar} } from '$lib/schemas'
15534
+ import { ${storeVar(schema)} as source } from '$lib/server/store'${imports}
15535
+
15536
+ ${handlers}
15537
+ `
15538
+ };
15539
+ }
15540
+ /**
15541
+ * `$lib/server/store.ts`: the seeded rows of every in-memory entity, held once
15542
+ * per server process. Only server code imports it (the API routes, scheduled
15543
+ * jobs), so the seed data stays out of the browser bundle.
15544
+ *
15545
+ * The server owns new ids. A browser's `nextId()` counter only knows its own
15546
+ * tab, so two tabs (or two people) would hand out the same id; a create whose
15547
+ * id is missing or already taken gets the next free one here instead.
15548
+ */
15549
+ function serverStoreFile(entries) {
15550
+ return {
15551
+ path: "src/lib/server/store.ts",
15552
+ description: "In-memory rows, held once on the server (seeded on start, reset on restart).",
15553
+ contents: `/**
15554
+ * The app's in-memory data, held once per server process and reached from the
15555
+ * browser through each entity's /api route. Seeded on start, reset on restart:
15556
+ * bind an entity to SQL in the Studio to keep its rows.
15557
+ */
15558
+ import { createInMemoryDataSource } from '@svgrid/enterprise'
15559
+ import type { EntitySchema } from '@svgrid/enterprise'
15560
+ import { ${entries.map((e) => namesFor(e.schema).schemaVar).join(", ")}, ${entries.map((e) => `type ${namesFor(e.schema).type}`).join(", ")} } from '$lib/schemas'
15561
+
15562
+ /** An in-memory source that assigns a free id when a create brings none, or a
15563
+ * taken one. \`prefix\` null means the id is numeric (max + 1). */
15564
+ function serverHeld<T extends Record<string, unknown>>(rows: T[], schema: EntitySchema<T>, idField: string, prefix: string | null) {
15565
+ const source = createInMemoryDataSource<T>(rows, schema)
15566
+ let seq = rows.length
15567
+ const freeId = (wanted: unknown): string | number => {
15568
+ const taken = new Set(source.rows().map((r) => String(r[idField])))
15569
+ if (wanted != null && wanted !== '' && !taken.has(String(wanted))) return wanted as string | number
15570
+ if (prefix === null) return Math.max(0, ...source.rows().map((r) => Number(r[idField]) || 0)) + 1
15571
+ let id = prefix + ++seq
15572
+ while (taken.has(id)) id = prefix + ++seq
15573
+ return id
15574
+ }
15575
+ return {
15576
+ ...source,
15577
+ createRow: (input: Partial<T>) => source.createRow({ ...input, [idField]: freeId(input[idField]) } as Partial<T>),
15578
+ }
15579
+ }
15580
+
15581
+ ${entries.map(({ schema, rows }) => {
15582
+ const n = namesFor(schema);
15583
+ const idField = resolveIdField(schema);
15584
+ const numeric = schema.fields.find((f) => f.field === idField)?.type === "number";
15585
+ return `export const ${storeVar(schema)} = serverHeld<${n.type}>(${JSON.stringify(rows)}, ${n.schemaVar}, ${JSON.stringify(idField)}, ${numeric ? "null" : JSON.stringify(n.idPrefix)})`;
15586
+ }).join("\n")}
15587
+ `
15588
+ };
15589
+ }
15590
+ /** The `createKitHandlers` call every entity route shares, plus the imports it needs. */
15591
+ function kitHandlersBlock(schema, feat) {
15592
+ const accessImport = feat.access ? `\nimport { authorizeAction, getServerRole } from '$lib/access'` : "";
15593
+ const auditImport = feat.audit ? `\nimport { recordAudit } from '$lib/audit'` : "";
15594
+ const tenantImport = feat.tenantField ? `\nimport { requireTenant } from '$lib/server/tenant'` : "";
15595
+ const opts = [
15596
+ `schema: ${namesFor(schema).schemaVar}`,
15597
+ `source`,
15598
+ `validate: true`
15599
+ ];
15600
+ if (feat.access) 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 ?? [])})`);
15601
+ if (feat.tenantField) 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) })`);
15602
+ if (feat.audit) 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') })`);
15603
+ const hooks = triggerHooksOpt(feat.triggers);
15604
+ if (hooks) opts.push(`// Business rules, enforced server-side (a client that skips them still can't write bad data).\n ${hooks}`);
15605
+ const handlers = `export const { POST } = createKitHandlers({\n ${opts.join(",\n ")},\n})`;
15606
+ return {
15607
+ imports: `${accessImport}${auditImport}${tenantImport}`,
15608
+ handlers
15609
+ };
15610
+ }
15334
15611
  /** Map an EntityField type to a Postgres column type for PGlite DDL. */
15335
15612
  var PG_COL_TYPE = {
15336
15613
  number: "double precision",
@@ -15469,7 +15746,7 @@ function sqlDdlFiles(schemas, sources) {
15469
15746
  contents: header("Sample seed rows (optional)") + inserts.join("\n\n") + "\n"
15470
15747
  }] : []];
15471
15748
  }
15472
- function dataModule(entries, seed, sources, supabaseConn) {
15749
+ function dataModule(entries, seed, sources, supabaseConn, serverMemory = false) {
15473
15750
  const schemas = entries.map((e) => e.schema);
15474
15751
  const typeImports = schemas.map((s) => `type ${pascal(s.name)}`).join(", ");
15475
15752
  const schemaImports = schemas.map((s) => namesFor(s).schemaVar).join(", ");
@@ -15477,7 +15754,8 @@ function dataModule(entries, seed, sources, supabaseConn) {
15477
15754
  const entImports = /* @__PURE__ */ new Set();
15478
15755
  const needs = {
15479
15756
  supabase: false,
15480
- sqlRoutes: []
15757
+ sqlRoutes: [],
15758
+ memoryRoutes: []
15481
15759
  };
15482
15760
  const stores = entries.map((e) => {
15483
15761
  const T = pascal(e.schema.name);
@@ -15517,9 +15795,17 @@ function dataModule(entries, seed, sources, supabaseConn) {
15517
15795
  });
15518
15796
  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>[] } })`;
15519
15797
  default: {
15798
+ const rowList = (src.kind === "memory" ? src.seed : void 0) ?? seed.get(e.schema.name) ?? [];
15799
+ if (serverMemory) {
15800
+ entImports.add("createKitDataSource");
15801
+ needs.memoryRoutes.push({
15802
+ schema: e.schema,
15803
+ rows: rowList
15804
+ });
15805
+ return `const ${store} = createKitDataSource<${T}>({ endpoint: '/api/${namesFor(e.schema).route}' })`;
15806
+ }
15520
15807
  entImports.add("createInMemoryDataSource");
15521
- const curated = src.kind === "memory" ? src.seed : void 0;
15522
- return `const ${store} = createInMemoryDataSource<${T}>(${JSON.stringify(curated ?? seed.get(e.schema.name) ?? [])}, ${sv})`;
15808
+ return `const ${store} = createInMemoryDataSource<${T}>(${JSON.stringify(rowList)}, ${sv})`;
15523
15809
  }
15524
15810
  }
15525
15811
  }).join("\n");
@@ -15983,7 +16269,7 @@ function prepareEntities(schemas) {
15983
16269
  /** Emit the shared entity modules: `src/lib/schemas.ts` + `src/lib/data.ts` (+ `connections.ts`). */
15984
16270
  function emitEntityModules(schemas, opts = {}) {
15985
16271
  const { entries, seed } = prepareEntities(schemas);
15986
- const { file: data, needs } = dataModule(entries, seed, opts.sources, opts.supabaseConn);
16272
+ const { file: data, needs } = dataModule(entries, seed, opts.sources, opts.supabaseConn, opts.serverMemory);
15987
16273
  const files = [schemasModule(entries), data];
15988
16274
  if ((needs.supabase || opts.supabaseAuth) && !needs.supabaseUrl && (opts.supabaseConn?.url || opts.supabaseConn?.key)) {
15989
16275
  needs.supabaseUrl = opts.supabaseConn.url;
@@ -15998,6 +16284,16 @@ function emitEntityModules(schemas, opts = {}) {
15998
16284
  dbSchema: r.dbSchema,
15999
16285
  tenantField: opts.tenantField && opts.tenantScoped?.(r.schema.name) !== false ? opts.tenantField : void 0
16000
16286
  }));
16287
+ if (needs.memoryRoutes.length) {
16288
+ files.push(serverStoreFile(needs.memoryRoutes));
16289
+ for (const r of needs.memoryRoutes) files.push(memoryRouteFile(r.schema, {
16290
+ access: opts.accessEnabled,
16291
+ audit: opts.auditEnabled,
16292
+ screenIds: opts.screensByEntity?.get(r.schema.name) ?? [],
16293
+ triggers: opts.triggers?.[r.schema.name],
16294
+ tenantField: opts.tenantField && opts.tenantScoped?.(r.schema.name) !== false ? opts.tenantField : void 0
16295
+ }));
16296
+ }
16001
16297
  return {
16002
16298
  files,
16003
16299
  prepared: entries.map((e) => e.schema)
@@ -16045,7 +16341,7 @@ function hasFieldConditions(schema) {
16045
16341
  *
16046
16342
  * `version.test.ts` asserts this matches `package.json`, so the two cannot drift.
16047
16343
  */
16048
- var SVGRID_VERSION = "3.1.4";
16344
+ var SVGRID_VERSION = "3.1.5";
16049
16345
  /**
16050
16346
  * The version of `@svgrid/grid`, which releases on its own line: enterprise is
16051
16347
  * on 3.1.x while the grid is on 3.0.x.
@@ -16058,7 +16354,7 @@ var SVGRID_VERSION = "3.1.4";
16058
16354
  *
16059
16355
  * `version.test.ts` asserts this matches packages/grid/package.json.
16060
16356
  */
16061
- var GRID_VERSION = "3.0.8";
16357
+ var GRID_VERSION = "3.0.9";
16062
16358
  //#endregion
16063
16359
  //#region src/studio/emit-project.ts
16064
16360
  var has = (blocks, kind) => blocks.some((b) => b.config.kind === kind);
@@ -18048,27 +18344,28 @@ function screenChildEntities(blocks, resolve) {
18048
18344
  }
18049
18345
  return [...out.values()];
18050
18346
  }
18051
- function ssrReadServerFile(schema, screen, sourceKind, accessEnabled, screenIds, resolve) {
18052
- const n = namesFor(schema);
18053
- const isSql = sourceKind === "sql";
18347
+ function ssrReadServerFile(schema, screen, routed, accessEnabled, screenIds, resolve) {
18054
18348
  const children = screenChildEntities(screen.blocks, resolve);
18055
- const rbac = accessEnabled && !isSql;
18056
- const srcExpr = (s) => isSql ? `createKitDataSource<Record<string, unknown>>({ endpoint: ${jsStr("/api/" + namesFor(s).route)}, fetch })` : namesFor(s).sourceVar;
18057
- const memImports = isSql ? [] : [.../* @__PURE__ */ new Set([n.sourceVar, ...children.map((c) => namesFor(c).sourceVar)])];
18349
+ const viaRoute = routed(schema.name);
18350
+ const rbac = accessEnabled && !viaRoute;
18351
+ const srcExpr = (s) => routed(s.name) ? `createKitDataSource<Record<string, unknown>>({ endpoint: ${jsStr("/api/" + namesFor(s).route)}, fetch })` : namesFor(s).sourceVar;
18352
+ const all = [schema, ...children];
18353
+ const anyRouted = all.some((s) => routed(s.name));
18354
+ const dataImports = [...new Set(all.filter((s) => !routed(s.name)).map((s) => namesFor(s).sourceVar))];
18058
18355
  const childLoads = children.map((c) => ` const ${mdChildVar(c.name)} = (await ${srcExpr(c)}.getRows(PAGE)).rows`).join("\n");
18059
18356
  return {
18060
18357
  path: `src/routes/${screen.route}/+page.server.ts`,
18061
18358
  description: `${screen.title} - SSR load (full dataset${children.length ? " + child collections" : ""}).`,
18062
18359
  contents: `import type { PageServerLoad } from './$types'
18063
18360
  import type { ServerRequest } from '@svgrid/grid'
18064
- ${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` : ""}
18361
+ ${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` : ""}
18065
18362
  // This screen renders on the server: real SSR HTML, not just a server-side
18066
18363
  // load. Stated explicitly so it holds whichever way the root layout is set.
18067
18364
  export const ssr = true
18068
18365
  ${rbac ? `\nconst SCREEN_IDS = ${JSON.stringify(screenIds)}\n` : ""}
18069
18366
  const PAGE: ServerRequest = { startRow: 0, endRow: 1000, pageIndex: 0, pageSize: 1000, sortModel: [], filterModel: {} }
18070
18367
 
18071
- export const load: PageServerLoad = async (${isSql || rbac ? `{ ${[isSql ? "fetch" : "", rbac ? "locals" : ""].filter(Boolean).join(", ")} }` : ""}) => {
18368
+ export const load: PageServerLoad = async (${anyRouted || rbac ? `{ ${[anyRouted ? "fetch" : "", rbac ? "locals" : ""].filter(Boolean).join(", ")} }` : ""}) => {
18072
18369
  ${rbac ? ` if (!authorizeAction(getServerRole({ locals }), 'read', SCREEN_IDS)) throw error(403, 'Not allowed')\n` : ""} const rows = (await ${srcExpr(schema)}.getRows(PAGE)).rows
18073
18370
  ${childLoads ? childLoads + "\n" : ""} return { rows${children.map((c) => `, ${mdChildVar(c.name)}`).join("")} }
18074
18371
  }
@@ -18135,10 +18432,9 @@ function attrs(f) {
18135
18432
  return out.length ? " " + out.join(" ") : "";
18136
18433
  }
18137
18434
  /** The `+page.server.ts` + SSR `+page.svelte` for a single-grid CRUD screen. */
18138
- function emitSsrGridScreen(schema, screen, sourceKind, accessEnabled, screenIds, byName) {
18435
+ function emitSsrGridScreen(schema, screen, routed, accessEnabled, screenIds, byName) {
18139
18436
  const n = namesFor(schema);
18140
- const isSql = sourceKind === "sql";
18141
- const isRest = sourceKind === "rest";
18437
+ const isSql = routed(schema.name);
18142
18438
  const relSchemaOf = (f) => byName.get(f.relation.entity);
18143
18439
  const relIdOf = (rs) => rs.idField ?? rs.fields.find((x) => x.primaryKey)?.field ?? "id";
18144
18440
  const grid = screen.blocks.find((b) => b.config.kind === "grid").config;
@@ -18153,30 +18449,32 @@ function emitSsrGridScreen(schema, screen, sourceKind, accessEnabled, screenIds,
18153
18449
  const fieldTypesLit = `{ ${formFields.map((f) => `${jsStr(f.field)}: ${jsStr(normType(f.type))}`).join(", ")} }`;
18154
18450
  const isRel = (f) => f.type === "relation" && !!f.relation && byName.has(f.relation.entity);
18155
18451
  const relFields = formFields.filter(isRel);
18156
- const relSourceVars = isSql ? [] : [...new Set(relFields.map((f) => namesFor(relSchemaOf(f)).sourceVar))];
18452
+ const relRouted = relFields.some((f) => routed(relSchemaOf(f).name));
18453
+ const relSourceVars = [...new Set(relFields.filter((f) => !routed(relSchemaOf(f).name)).map((f) => namesFor(relSchemaOf(f)).sourceVar))];
18157
18454
  const relPrefetch = relFields.map((f) => {
18158
18455
  const rs = relSchemaOf(f);
18159
18456
  const rel = namesFor(rs);
18160
18457
  const relId = relIdOf(rs);
18161
18458
  const labelF = f.relation.labelField ?? relId;
18162
- const relSource = isSql ? `createKitDataSource<Record<string, unknown>>({ endpoint: ${jsStr("/api/" + rel.route)}, fetch })` : rel.sourceVar;
18459
+ const relSource = routed(rs.name) ? `createKitDataSource<Record<string, unknown>>({ endpoint: ${jsStr("/api/" + rel.route)}, fetch })` : rel.sourceVar;
18163
18460
  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)}] ?? '') }))`;
18164
18461
  }).join("\n");
18165
18462
  const relReturn = relFields.map((f) => `, ${f.field}Options`).join("");
18166
18463
  const src = isSql ? "source(fetch)" : n.sourceVar;
18167
18464
  const fetchArg = isSql ? ", fetch" : "";
18465
+ const loadFetchArg = isSql || relRouted ? ", fetch" : "";
18168
18466
  const conditional = hasFieldConditions(schema);
18169
18467
  const readValues = (fd) => conditional ? `stripHiddenValues(${n.schemaVar}, formToValues(${fd}))` : `formToValues(${fd})`;
18170
18468
  const enterpriseImports = [
18171
18469
  "validateAll",
18172
18470
  ...conditional ? ["stripHiddenValues"] : [],
18173
- ...isSql ? ["createKitDataSource"] : []
18471
+ ...isSql || relRouted ? ["createKitDataSource"] : []
18174
18472
  ].join(", ");
18175
- const sourceImport = isSql ? "" : `import { ${[.../* @__PURE__ */ new Set([n.sourceVar, ...relSourceVars]), ...isRest ? [] : ["nextId"]].join(", ")} } from '$lib/data'\n`;
18473
+ const dataImports = [.../* @__PURE__ */ new Set([...isSql ? [] : [n.sourceVar], ...relSourceVars])];
18474
+ const sourceImport = dataImports.length ? `import { ${dataImports.join(", ")} } from '$lib/data'\n` : "";
18176
18475
  const schemaImport = `import { ${n.schemaVar}${isSql ? `, type ${n.type}` : ""} } from '$lib/schemas'`;
18177
- const idConst = isSql || isRest ? "" : `\nconst ID_FIELD = ${jsStr(idField)}`;
18178
18476
  const srcHelper = isSql ? `\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` : "";
18179
- const createCall = isSql || isRest ? `await ${src}.createRow(values)` : `await ${n.sourceVar}.createRow({ [ID_FIELD]: nextId(${jsStr(n.idPrefix)}), ...values })`;
18477
+ const createCall = `await ${src}.createRow(values)`;
18180
18478
  const rbac = accessEnabled && !isSql;
18181
18479
  const localsArg = rbac ? ", locals" : "";
18182
18480
  const readGuard = rbac ? ` if (!authorizeAction(getServerRole({ locals }), 'read', SCREEN_IDS)) throw error(403, 'Not allowed')\n` : "";
@@ -18190,7 +18488,7 @@ import { planFromSearchParams } from '$lib/server/query'
18190
18488
  // This screen renders on the server: real SSR HTML, not just a server-side
18191
18489
  // load. Stated explicitly so it holds whichever way the root layout is set.
18192
18490
  export const ssr = true
18193
- ${idConst}${rbac ? `\nconst SCREEN_IDS = ${JSON.stringify(screenIds)}` : ""}
18491
+ ${rbac ? `\nconst SCREEN_IDS = ${JSON.stringify(screenIds)}` : ""}
18194
18492
  const FIELD_TYPES: Record<string, 'text' | 'number' | 'boolean'> = ${fieldTypesLit}
18195
18493
  ${srcHelper}
18196
18494
  /** Read a submitted form into a typed partial row. Booleans come from checkbox
@@ -18206,7 +18504,7 @@ function formToValues(fd: FormData): Record<string, unknown> {
18206
18504
  return values
18207
18505
  }
18208
18506
 
18209
- export const load: PageServerLoad = async ({ url${fetchArg}${localsArg} }) => {
18507
+ export const load: PageServerLoad = async ({ url${loadFetchArg}${localsArg} }) => {
18210
18508
  ${readGuard} const plan = planFromSearchParams(url, ${pageSize})
18211
18509
  const { rows, rowCount } = await ${src}.getRows(plan)
18212
18510
  ${relPrefetch ? relPrefetch + "\n" : ""} return { rows, total: rowCount, page: plan.pageIndex, size: plan.pageSize, sort: plan.sortModel${relReturn} }
@@ -18390,14 +18688,26 @@ ${g.title ? ` <legend>${htmlEsc(g.title)}</legend>\n` : ""}${inner}
18390
18688
  const q = sp.toString()
18391
18689
  void goto(q ? \`?\${q}\` : page.url.pathname, { keepFocus: true, noScroll: true })
18392
18690
  }
18691
+
18692
+ // Only a sort the URL does not already hold navigates. Grid releases before
18693
+ // the mount-report fix also reported the starting sort when they mounted,
18694
+ // and acting on that would drop ?page on every load.
18695
+ function onSort(s: Array<{ id: string; desc: boolean }>) {
18696
+ const sort = s.map((x) => \`\${x.id}:\${x.desc ? 'desc' : 'asc'}\`).join(',')
18697
+ if (sort !== (page.url.searchParams.get('sort') ?? '')) setParams({ sort: sort || null, page: null })
18698
+ }
18393
18699
  ${wantsFilter ? `
18394
18700
  // Filter via the URL (q = global search, f_<col> = per-column). The server's
18395
18701
  // planFromSearchParams reads these, so load() re-filters; reset to the first page.
18396
18702
  function applyFilters(f: { global: string; columns: Array<{ id: string; value: string }> }) {
18397
18703
  const sp = new URLSearchParams(page.url.searchParams)
18704
+ const filterParams = (p: URLSearchParams) => [...p].filter(([k]) => k === 'q' || k.startsWith('f_')).sort().join('&')
18705
+ const before = filterParams(sp)
18398
18706
  for (const k of [...sp.keys()]) if (k === 'q' || k.startsWith('f_')) sp.delete(k)
18399
18707
  if (f.global) sp.set('q', f.global)
18400
18708
  for (const c of f.columns) if (c.value) sp.set('f_' + c.id, c.value)
18709
+ // Nothing changed (older grid releases report the starting filters on mount).
18710
+ if (filterParams(sp) === before) return
18401
18711
  sp.delete('page')
18402
18712
  const q = sp.toString()
18403
18713
  void goto(q ? \`?\${q}\` : page.url.pathname, { keepFocus: true, noScroll: true })
@@ -18431,12 +18741,13 @@ ${facetForm}
18431
18741
  {columns}
18432
18742
  externalSort
18433
18743
  initialSorting={data.sort}
18744
+ showPagination
18434
18745
  externalPagination${wantsFilter ? "\n filterable\n externalFilter\n onFiltersChange={applyFilters}" : ""}
18435
18746
  rowCount={data.total}
18436
18747
  pageIndex={data.page}
18437
18748
  pageSize={data.size}
18438
- onSortingChange={(s) => setParams({ sort: s.map((x) => \`\${x.id}:\${x.desc ? 'desc' : 'asc'}\`).join(',') || null, page: null })}
18439
- onPaginationChange={(p) => setParams({ page: String(p.pageIndex), size: String(p.pageSize) })}
18749
+ onSortingChange={onSort}
18750
+ onPaginationChange={(p) => { if (p.pageIndex !== data.page || p.pageSize !== data.size) setParams({ page: String(p.pageIndex), size: String(p.pageSize) }) }}
18440
18751
  />
18441
18752
 
18442
18753
  {#if editing}
@@ -18502,7 +18813,8 @@ function emitStudioProject(project) {
18502
18813
  const sources = Object.fromEntries(project.entities.map((e) => [e.name, entityDataSource(project, e.name)]));
18503
18814
  const accessEnabled = project.access?.enabled === true && (project.access?.roles.length ?? 0) > 0;
18504
18815
  const authEnabled = project.auth?.enabled === true;
18505
- const auditEnabled = project.audit === true && Object.values(sources).some((s) => s.kind === "sql");
18816
+ const routed = (entity) => sources[entity]?.kind === "sql" || sources[entity]?.kind === "memory";
18817
+ const auditEnabled = project.audit === true && project.entities.some((e) => routed(e.name));
18506
18818
  const i18nEnabled = project.i18n?.enabled === true && (project.i18n?.locales.length ?? 0) > 0;
18507
18819
  const screensByEntity = /* @__PURE__ */ new Map();
18508
18820
  for (const s of project.screens) {
@@ -18519,7 +18831,8 @@ function emitStudioProject(project) {
18519
18831
  supabaseConn: project.supabase,
18520
18832
  supabaseAuth: project.auth?.enabled === true && project.auth.provider === "supabase",
18521
18833
  tenantField: tenantCol,
18522
- tenantScoped: (name) => isTenantScoped(project, name)
18834
+ tenantScoped: (name) => isTenantScoped(project, name),
18835
+ serverMemory: true
18523
18836
  });
18524
18837
  const byName = new Map(prepared.map((s) => [s.name, s]));
18525
18838
  const rawByName = new Map(project.entities.map((e) => [e.name, e]));
@@ -18549,10 +18862,8 @@ function emitStudioProject(project) {
18549
18862
  const schema = byName.get(screen.entity);
18550
18863
  if (!schema) throw new Error(`emitStudioProject: screen "${screen.title}" references missing entity "${screen.entity}"`);
18551
18864
  if (isSsrScreen(project, screen)) {
18552
- const kind = sources[screen.entity]?.kind;
18553
- const srcKind = kind === "sql" ? "sql" : kind === "rest" ? "rest" : "memory";
18554
- if (ssrScreenShape(project, screen) === "grid") pages.push(...emitSsrGridScreen(schema, screen, srcKind, accessEnabled, screensByEntity.get(screen.entity) ?? [], byName));
18555
- else 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));
18865
+ if (ssrScreenShape(project, screen) === "grid") pages.push(...emitSsrGridScreen(schema, screen, routed, accessEnabled, screensByEntity.get(screen.entity) ?? [], byName));
18866
+ else 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));
18556
18867
  continue;
18557
18868
  }
18558
18869
  pages.push(screenPage(schema, rawByName.get(screen.entity) ?? schema, screen, resolve, (name) => rawByName.get(name), accessEnabled, i18nEnabled, routeById, drillEnabled, false, sources[screen.entity]));
@@ -18912,6 +19223,8 @@ export function requireTenant(event: TenantEvent): string {
18912
19223
  * route checks `CRON_SECRET` and runs the due handlers.
18913
19224
  */
18914
19225
  function jobsFiles(project, jobs, emailAvailable) {
19226
+ const isServerHeld = (ent) => entityDataSource(project, ent.name).kind === "memory";
19227
+ const jobSourceVar = (ent) => isServerHeld(ent) ? storeVar(ent) : namesFor(ent).sourceVar;
18915
19228
  const handler = (j) => {
18916
19229
  const label = jsStr(j.name);
18917
19230
  if (j.kind === "email") {
@@ -18922,9 +19235,8 @@ function jobsFiles(project, jobs, emailAvailable) {
18922
19235
  const why = !emailAvailable ? "email is not enabled on this project" : !j.to ? "no recipient set" : `unknown entity ${j.entity}`;
18923
19236
  return ` ${JSON.stringify(j.id)}: async () => {\n // ${label}: cannot send - ${why}.\n console.warn('cron ${j.id}: skipped (${why})')\n },`;
18924
19237
  }
18925
- const n = namesFor(ent);
18926
19238
  return ` ${JSON.stringify(j.id)}: async () => {
18927
- const { rows, rowCount } = await ${n.sourceVar}.getRows({ startRow: 0, endRow: 10, sortModel: [], filterModel: {} })
19239
+ const { rows, rowCount } = await ${jobSourceVar(ent)}.getRows({ startRow: 0, endRow: 10, sortModel: [], filterModel: {} })
18928
19240
  const items = rows.map((r) => '<li>' + Object.values(r).slice(0, 3).map(String).join(' &middot; ') + '</li>').join('')
18929
19241
  await sendEmail(${to}, ${subject}, '<p>' + rowCount + ' ${ent.name} total. Most recent:</p><ul>' + items + '</ul>')
18930
19242
  },`;
@@ -18933,19 +19245,26 @@ function jobsFiles(project, jobs, emailAvailable) {
18933
19245
  return ` ${JSON.stringify(j.id)}: async () => {\n${body.split("\n").map((l) => " " + l).join("\n")}\n },`;
18934
19246
  };
18935
19247
  const entityImports = /* @__PURE__ */ new Set();
19248
+ const storeImports = /* @__PURE__ */ new Set();
18936
19249
  for (const j of jobs) {
18937
19250
  if (j.kind !== "email") continue;
18938
19251
  const ent = project.entities.find((e) => e.name === j.entity);
18939
- if (ent && emailAvailable && j.to) entityImports.add(namesFor(ent).sourceVar);
19252
+ if (!ent || !emailAvailable || !j.to) continue;
19253
+ if (isServerHeld(ent)) storeImports.add(jobSourceVar(ent));
19254
+ else entityImports.add(jobSourceVar(ent));
18940
19255
  }
18941
- const imports = [...entityImports.size ? [`import { ${[...entityImports].sort().join(", ")} } from '$lib/data'`] : [], ...jobs.some((j) => j.kind === "email") && emailAvailable ? [`import { sendEmail } from '$lib/server/email'`] : []];
19256
+ const imports = [
19257
+ ...entityImports.size ? [`import { ${[...entityImports].sort().join(", ")} } from '$lib/data'`] : [],
19258
+ ...storeImports.size ? [`import { ${[...storeImports].sort().join(", ")} } from '$lib/server/store'`] : [],
19259
+ ...jobs.some((j) => j.kind === "email") && emailAvailable ? [`import { sendEmail } from '$lib/server/email'`] : []
19260
+ ];
18942
19261
  return [{
18943
19262
  path: "src/lib/server/jobs.ts",
18944
19263
  description: "Scheduled job handlers, keyed by job id.",
18945
19264
  contents: `// Regenerated by SvGrid Studio. Scheduled job handlers.
18946
19265
  //
18947
19266
  // Schedule (UTC):
18948
- ${jobs.map((j) => ` * ${j.id.padEnd(20)} ${j.cron.padEnd(16)} ${j.name}`).join("\n")}
19267
+ ${jobs.map((j) => `// ${j.id.padEnd(20)} ${j.cron.padEnd(16)} ${j.name}`).join("\n")}
18949
19268
  //
18950
19269
  // Runs on the server, triggered by /api/cron - NOT in the browser. Edit a
18951
19270
  // handler body freely; the registry keys are what /api/cron dispatches on.
@@ -20548,8 +20867,8 @@ function rootLayoutModule(project) {
20548
20867
  const anySsr = project.screens.some((s) => isSsrScreen(project, s));
20549
20868
  return {
20550
20869
  path: "src/routes/+layout.ts",
20551
- description: anySsr ? "Root layout options (SSR on; client-only screens opt out per page)." : "Client SPA (in-memory sources persist across navigation).",
20552
- contents: anySsr ? `// 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` : `// 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`
20870
+ description: anySsr ? "Root layout options (SSR on; client-only screens opt out per page)." : "Client SPA (no screen renders on the server).",
20871
+ contents: anySsr ? `// 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` : `// No screen here renders on the server, so the app runs as a client SPA.\nexport const ssr = false\nexport const prerender = false\n`
20553
20872
  };
20554
20873
  }
20555
20874
  /** The static SvelteKit + Vite scaffolding around the generated screens. */
@@ -21099,7 +21418,7 @@ function emitStudioAppBundle(project) {
21099
21418
  const plan = deployPlan(project);
21100
21419
  const deploySteps = plan.dashboard ? `1. Push this folder to a Git repo (GitHub / GitLab / Bitbucket).\n2. Import it at <${plan.dashboard}> - build settings are detected automatically.\n\nOr deploy straight from your machine with the CLI:\n\n\`\`\`bash\n${plan.cli}\n\`\`\`` : `Build and run the server:\n\n\`\`\`bash\n${plan.cli}\n\`\`\``;
21101
21420
  const readme = `# ${project.title}\n\nGenerated with SvGrid Studio.\n\n\`\`\`bash\nnpm install\nnpm run dev\n\`\`\`\n\nSQL / Supabase entities read their connection from \`.env\` (\`DATABASE_URL\`) or\n\`src/lib/connections.ts\`. Everything else runs on seeded in-memory data.\n\n## Deploy (${plan.label})\n\nThis app is configured for **${plan.label}** (SvelteKit \`${plan.adapterModule}\`).\n\n${deploySteps}\n\nTo target a different host, pick another **Deploy target** in the designer and\nre-generate, or swap the adapter in \`svelte.config.js\`. Entities on **Local\ndatabase** (PGlite) or **In-memory** need no server env; SQL / Supabase entities\nneed their connection set in the host's environment variables.\n\n## Round-tripping back into the designer\n\nThis app ships its own design model in \`studio.config.json\`. To keep editing\nvisually, open the SvGrid Studio designer and **Load** that file - your entities,\nscreens, blocks, theme, RBAC, i18n, etc. come back exactly as generated, and you\ncan re-generate from there.\n\nThe designer regenerates the files under \`src/routes\` and \`src/lib\` from the\nmodel, so **keep your own custom code in new files/modules and import it**, rather\nthan editing the generated screens in place - that way a re-generate never\nclobbers your work. (The CLI workflow, \`npx @svgrid/studio add\`, is the\nalternative: it wraps generated code in \`svgrid:managed\` markers and preserves\nanything you write outside them.)\n`;
21102
- return [...[
21421
+ const bundle = [...[
21103
21422
  {
21104
21423
  path: "package.json",
21105
21424
  description: "Dependencies + scripts (npm install, npm run dev).",
@@ -21173,6 +21492,7 @@ function emitStudioAppBundle(project) {
21173
21492
  })
21174
21493
  }
21175
21494
  ], ...generated];
21495
+ return project.kit === 3 ? toSvelteKit3(bundle) : bundle;
21176
21496
  }
21177
21497
  /**
21178
21498
  * Emit ONLY the app content (`src/routes` + `src/lib` + `db`) to drop into an
@@ -33241,7 +33561,7 @@ var restaurant = {
33241
33561
  order: 2
33242
33562
  }, {
33243
33563
  startField: "reservationTime",
33244
- titleField: "name",
33564
+ titleField: "customerPhone",
33245
33565
  colorField: "status",
33246
33566
  resourceField: "tableId",
33247
33567
  initialView: "day"
@@ -41501,7 +41821,7 @@ async function runStudioInit(flags, prompts, db, io, fetchText = null) {
41501
41821
  const title = flags.yes || flags.title ? defaultTitle : (await prompts.ask("\nApp name:", defaultTitle)).trim() || defaultTitle;
41502
41822
  const outDir = flags.out ?? (flags.yes ? "." : (await prompts.ask("Write it where?", ".")).trim() || ".");
41503
41823
  await confirmTargetFolder(outDir, flags, prompts, io, outDir === candidateDir ? markersBefore : null);
41504
- const project = crudAppFromSchemas(data.schemas, {
41824
+ const built = crudAppFromSchemas(data.schemas, {
41505
41825
  title,
41506
41826
  dataSource: data.kind,
41507
41827
  sources: data.sources,
@@ -41510,6 +41830,7 @@ async function runStudioInit(flags, prompts, db, io, fetchText = null) {
41510
41830
  overviewDashboard: true,
41511
41831
  ...theme ? { theme } : {}
41512
41832
  });
41833
+ const project = flags.kit === 3 ? setKitVersion(built, 3) : built;
41513
41834
  const written = await writeAll(emitStudioAppBundle(project), outDir, io);
41514
41835
  const configPath = inDir(outDir, "studio.config.json");
41515
41836
  await io.writeFile(configPath, serializeProject(project));
@@ -41967,4 +42288,4 @@ function checkLicenseKey(key, now = /* @__PURE__ */ new Date()) {
41967
42288
  };
41968
42289
  }
41969
42290
  //#endregion
41970
- export { CANVAS_COLS, CANVAS_GAP_PX, CANVAS_ROW_PX, CONTAINER_CHILD_KINDS, CONTAINER_DISPLAY_KINDS, CRUD_ACTIONS, DEFAULT_PORT, DRIVER_PACKAGE, FORM_SUBMIT, GRID_API_MEMBERS, GRID_CURATED_PROPS, GRID_EVENTS, GRID_PRESETS, MANAGED_END, MANAGED_START, ON_LOAD, PANE_LAYOUTS, STANDARD_UI_EVENTS, TAB_CHILD_KINDS, TRIGGER_EVENTS, UI_COMPONENT_REGISTRY, UserError, addAccordionBlock, addAccordionComponent, addAccordionSection, addBlock, addBlockAt, addComponentBlock, addCrudSuite, addEntity, addFormSection, addFreestandingScreen, addHandlerStep, addScreen, addScreenAction, addScreenFromTemplate, addStateVar, addTab, addTabBlock, addTabComponent, applyGridPreset, blockClassName, blockColumns, blockPalette, blockStyleCss, buildCanvasLayout, buildConnectionString, buildCopilotMessages, buildDockLayout, buildStudioBugReport, canvasOpts, canvasRectOf, changeSlot, checkLicenseKey, clickSlot, compileCondition, compileHandlerSteps, compileStep, compileTriggerStep, compileTriggerSteps, compileValue, componentHandleMembers, componentHandleName, componentHasBindings, countTableRows, createProject, crudAppFromSchemas, crudSuiteScreens, csvToEntity, ctxAmbientDts, ctxCompletions, dashScreen, defaultBlockConfig, defaultEntitySource, defaultScreenFor, defaultStudioTheme, deployCommands, detailScreen, detectDelimiter, disableScreenCode, dockOpts, dockPaneIds, dockPaneTitleOf, duplicateBlock, duplicateScreen, emitEntityModules, emitStudioApp, emitStudioAppBundle, emitStudioFragment, emitStudioProject, enableScreenCode, entityDataSource, entityOf, entityScreenPage, eventSlot, flattenBlocks, formControlSettings, formControlsFor, formPlan, formScreen, generateRows, generateValue, getLiveDataSample, getSampleApp, getStarterDataset, getStudioTheme, gridApiSettableProps, gridColumns, gridConfig, gridIconGroups, gridOpts, gridPresetLabel, gridPropSurface, inferType, insertBlock, introspectDatabase, introspectDrizzle, introspectDrizzleAll, introspectJson, introspectOpenApi, introspectPrisma, introspectPrismaAll, isDarkTheme, isFieldHidden, isFileDialect, isPaneLayout, isProjectValid, isTenantScoped, isUserError, linkRelationLabels, listDatabaseTables, listScreen, liveDataSamples, mapSqlType, mergeBlockStyle, mergeManaged, missingEnvKeys, moveBlock, moveFormField, moveFormFields, moveFormSection, moveHandlerStep, parseConnectionString, parseCsv, parseProject, parseSelection, pickLabelField, prepareEntities, probeConnection, projectFromModelText, reconcileDock, redactConnectionString, refineField, refineFields, removeAccordionBlock, removeAccordionSection, removeBlock, removeEntity, removeFormSection, removeScreen, removeScreenAction, removeStateVar, removeTab, removeTabBlock, renameAccordionSection, renameTab, reorderBlock, reorderScreen, reseedScreenDock, resolveDeployTarget, resolveIdField, resolveSchema, resolveSchemas, resolveThemeTokens, resolveThemeTokensFor, roleCanAction, roleCanScreen, rowSelectSlot, runStudioAdd, runStudioAddApp, runStudioInit, runtimeDeps, sampleApps, sanitizeClassName, sanitizeProject, sanitizeStudioProject, scaffold, scaffoldApp, schemaToColumns, schemaToFormFields, screenDataset, screenFromTemplate, screenHandles, screenLayoutOf, seedUsers, serializeProject, setAccordionMultiple, setAuth, setCanvasRect, setComponentBinding, setComponentName, setDataLayer, setDataSource, setDeployTarget, setDockPaneTitle, setEntityDataSource, setEntityForm, setFieldConditions, setFieldHidden, setFieldInput, setFormColumns, setHandlerBody, setHandlerSteps, setJob, setLayoutOpts, setScreenDock, setScreenHandlersSource, setScreenLayout, setScreenRenderGrid, setScreenRenderMode, setShell, setTenancy, setTheme, setThemePreset, setTrigger, skipUserOwned, splitOpts, ssrEligible, ssrScreenShape, stackOpts, starterDatasets, starterProject, stateInitExpr, stateTsType, statusPills, stepsToCode, studioDeployInfo, studioThemes, suggestFormSections, summarizeVerify, syncDockPanes, tenantField, themeStyleString, triggersOf, uiComponentSpec, updateBlock, updateEntity, updateEntityField, updateFormSection, updateHandlerStep, updateScreen, updateStateVar, validateProject, verifyScaffold, withSsrDefaults };
42291
+ export { CANVAS_COLS, CANVAS_GAP_PX, CANVAS_ROW_PX, CONTAINER_CHILD_KINDS, CONTAINER_DISPLAY_KINDS, CRUD_ACTIONS, DEFAULT_PORT, DRIVER_PACKAGE, FORM_SUBMIT, GRID_API_MEMBERS, GRID_CURATED_PROPS, GRID_EVENTS, GRID_PRESETS, MANAGED_END, MANAGED_START, ON_LOAD, PANE_LAYOUTS, STANDARD_UI_EVENTS, SVELTEKIT_3_ADAPTERS, SVELTEKIT_3_DEV_DEPENDENCIES, TAB_CHILD_KINDS, TRIGGER_EVENTS, UI_COMPONENT_REGISTRY, UserError, addAccordionBlock, addAccordionComponent, addAccordionSection, addBlock, addBlockAt, addComponentBlock, addCrudSuite, addEntity, addFormSection, addFreestandingScreen, addHandlerStep, addScreen, addScreenAction, addScreenFromTemplate, addStateVar, addTab, addTabBlock, addTabComponent, applyGridPreset, blockClassName, blockColumns, blockPalette, blockStyleCss, buildCanvasLayout, buildConnectionString, buildCopilotMessages, buildDockLayout, buildStudioBugReport, canvasOpts, canvasRectOf, changeSlot, checkLicenseKey, clickSlot, compileCondition, compileHandlerSteps, compileStep, compileTriggerStep, compileTriggerSteps, compileValue, componentHandleMembers, componentHandleName, componentHasBindings, countTableRows, createProject, crudAppFromSchemas, crudSuiteScreens, csvToEntity, ctxAmbientDts, ctxCompletions, dashScreen, defaultBlockConfig, defaultEntitySource, defaultScreenFor, defaultStudioTheme, deployCommands, detailScreen, detectDelimiter, disableScreenCode, dockOpts, dockPaneIds, dockPaneTitleOf, duplicateBlock, duplicateScreen, emitEntityModules, emitStudioApp, emitStudioAppBundle, emitStudioFragment, emitStudioProject, enableScreenCode, entityDataSource, entityOf, entityScreenPage, eventSlot, flattenBlocks, formControlSettings, formControlsFor, formPlan, formScreen, generateRows, generateValue, getLiveDataSample, getSampleApp, getStarterDataset, getStudioTheme, gridApiSettableProps, gridColumns, gridConfig, gridIconGroups, gridOpts, gridPresetLabel, gridPropSurface, inferType, insertBlock, introspectDatabase, introspectDrizzle, introspectDrizzleAll, introspectJson, introspectOpenApi, introspectPrisma, introspectPrismaAll, isDarkTheme, isFieldHidden, isFileDialect, isPaneLayout, isProjectValid, isTenantScoped, isUserError, linkRelationLabels, listDatabaseTables, listScreen, liveDataSamples, mapSqlType, mergeBlockStyle, mergeManaged, missingEnvKeys, moveBlock, moveFormField, moveFormFields, moveFormSection, moveHandlerStep, parseConnectionString, parseCsv, parseProject, parseSelection, pickLabelField, prepareEntities, probeConnection, projectFromModelText, reconcileDock, redactConnectionString, refineField, refineFields, removeAccordionBlock, removeAccordionSection, removeBlock, removeEntity, removeFormSection, removeScreen, removeScreenAction, removeStateVar, removeTab, removeTabBlock, renameAccordionSection, renameTab, reorderBlock, reorderScreen, reseedScreenDock, resolveDeployTarget, resolveIdField, resolveSchema, resolveSchemas, resolveThemeTokens, resolveThemeTokensFor, roleCanAction, roleCanScreen, rowSelectSlot, runStudioAdd, runStudioAddApp, runStudioInit, runtimeDeps, sampleApps, sanitizeClassName, sanitizeProject, sanitizeStudioProject, scaffold, scaffoldApp, schemaToColumns, schemaToFormFields, screenDataset, screenFromTemplate, screenHandles, screenLayoutOf, seedUsers, serializeProject, setAccordionMultiple, setAuth, setCanvasRect, setComponentBinding, setComponentName, setDataLayer, setDataSource, setDeployTarget, setDockPaneTitle, setEntityDataSource, setEntityForm, setFieldConditions, setFieldHidden, setFieldInput, setFormColumns, setHandlerBody, setHandlerSteps, setJob, setKitVersion, setLayoutOpts, setScreenDock, setScreenHandlersSource, setScreenLayout, setScreenRenderGrid, setScreenRenderMode, setShell, setTenancy, setTheme, setThemePreset, setTrigger, skipUserOwned, splitOpts, ssrEligible, ssrScreenShape, stackOpts, starterDatasets, starterProject, stateInitExpr, stateTsType, statusPills, stepsToCode, studioDeployInfo, studioThemes, suggestFormSections, summarizeVerify, syncDockPanes, tenantField, themeStyleString, toSvelteKit3, triggersOf, uiComponentSpec, updateBlock, updateEntity, updateEntityField, updateFormSection, updateHandlerStep, updateScreen, updateStateVar, validateProject, verifyScaffold, withSsrDefaults };