@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.
- package/dist/cdn/svgrid-enterprise.svelte-external.js +8956 -8606
- package/dist/designer/assets/GridMenus-CvcASlKv.js +8 -0
- package/dist/designer/assets/SvListBox-BlmmzYcU.js +110 -0
- package/dist/designer/assets/{index-BgVTSVoK.js → index-B3WBezq_.js} +432 -370
- package/dist/designer/assets/index-KmSEY7Lb.css +1 -0
- package/dist/designer/assets/src-D4PRZJYA.js +58 -0
- package/dist/designer/assets/{validate-YxSrBF1H.js → validate-B71dE1Cs.js} +1 -1
- package/dist/designer/index.html +4 -4
- package/dist/node/studio.js +402 -81
- package/dist/server/server-row-model.js +199 -26
- package/dist/studio/emit-project.js +86 -56
- package/dist/studio/emit-schema.d.ts +3 -0
- package/dist/studio/emit-schema.js +128 -29
- package/dist/studio/index.d.ts +2 -1
- package/dist/studio/index.js +2 -1
- package/dist/studio/init-flow.d.ts +3 -0
- package/dist/studio/init-flow.js +5 -2
- package/dist/studio/project.d.ts +10 -1
- package/dist/studio/project.js +28 -15
- package/dist/studio/samples/restaurant.js +1 -1
- package/dist/studio/sveltekit3.d.ts +38 -0
- package/dist/studio/sveltekit3.js +194 -0
- package/dist/studio/ui-components.generated.js +15 -4
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/watermark.js +31 -1
- package/dist/wc/{GridMenus-6_fF5jIm.js → GridMenus-fWbBUiRH.js} +121 -121
- package/dist/wc/sv-sheet-element.js +10979 -10522
- package/dist/wc/{svelte-virtualizer.svelte-Du4tjrIP.js → svelte-virtualizer.svelte-Dntdh8Ms.js} +103 -56
- package/dist/wc/{validate-JAuMsk6Z.js → validate-43C03DLP.js} +1 -1
- package/package.json +3 -3
- package/dist/designer/assets/GridMenus-DGZJKFvX.js +0 -8
- package/dist/designer/assets/SvListBox-v2jm0-vf.js +0 -110
- package/dist/designer/assets/index-l21iGkpF.css +0 -1
- 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,
|
|
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
|
|
2431
|
-
const
|
|
2432
|
-
const
|
|
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
|
-
${
|
|
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 (${
|
|
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,
|
|
2530
|
+
function emitSsrGridScreen(schema, screen, routed, accessEnabled, screenIds, byName) {
|
|
2529
2531
|
const n = namesFor(schema);
|
|
2530
|
-
|
|
2531
|
-
//
|
|
2532
|
-
// called directly
|
|
2533
|
-
//
|
|
2534
|
-
const
|
|
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
|
|
2554
|
-
//
|
|
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
|
-
|
|
2558
|
-
|
|
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 =
|
|
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
|
|
2574
|
-
// -
|
|
2575
|
-
//
|
|
2576
|
-
//
|
|
2577
|
-
//
|
|
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
|
|
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 =
|
|
2599
|
-
|
|
2600
|
-
|
|
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
|
-
${
|
|
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${
|
|
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={
|
|
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
|
-
//
|
|
2966
|
-
|
|
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,
|
|
3045
|
+
pages.push(...emitSsrGridScreen(schema, screen, routed, accessEnabled, screensByEntity.get(screen.entity) ?? [], byName));
|
|
3033
3046
|
}
|
|
3034
3047
|
else {
|
|
3035
|
-
pages.push(ssrReadServerFile(schema, screen,
|
|
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 ${
|
|
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(' · ') + '</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
|
|
3412
|
-
|
|
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
|
-
|
|
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 (
|
|
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 -
|
|
4869
|
-
: `//
|
|
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
|
-
|
|
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
|
|
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'${
|
|
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
|
|
544
|
-
|
|
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
|
/**
|
package/dist/studio/index.d.ts
CHANGED
|
@@ -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';
|