@happyvertical/smrt-template-sveltekit 0.37.6 → 0.37.8
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/AGENTS.md +4 -1
- package/README.md +4 -1
- package/package.json +2 -2
- package/template/AGENTS.md +9 -0
- package/template/README.md +122 -1
- package/template/package.json +3 -3
- package/template/src/hooks.server.ts +5 -4
- package/template/src/lib/server/smrt.ts +33 -3
- package/template/src/routes/+page.server.ts +107 -0
- package/template/src/routes/+page.svelte +92 -26
- package/template/vite.config.ts +17 -0
package/AGENTS.md
CHANGED
|
@@ -12,8 +12,10 @@ Base SvelteKit project template used by `smrt init`. Scaffolds a full-stack, mul
|
|
|
12
12
|
|
|
13
13
|
- `template/src/hooks.server.ts` — pre-wires `enableTenancy()`, `createSessionHandler({ enterTenantContext: true })`, and a subdomain → tenantId handle, sequenced in that order
|
|
14
14
|
- `template/src/lib/server/tenancy.ts` — pluggable tenant resolver (`subdomainStrategy`, `pathPrefixStrategy`, `headerStrategy`, `createTenantResolver`)
|
|
15
|
-
- `template/src/lib/server/smrt.ts` — centralized SmrtClassOptions / collection factory
|
|
15
|
+
- `template/src/lib/server/smrt.ts` — centralized SmrtClassOptions / collection factory; imports the plugin-generated `smrt-register.js` (guarded — the file is gitignored and regenerated on every dev/build run) and seeds `.smrt/manifest.json` so server runtimes get package-qualified registrations + scanned field metadata (without them, writes silently drop domain columns)
|
|
16
|
+
- `template/vite.config.ts` — smrtPlugin() + smrtConsumer(), plus explicit `oxc: { decorator: { legacy: true } }` (Vite 8's oxc transform does not reliably honor `experimentalDecorators` through the SvelteKit tsconfig `extends` chain)
|
|
16
17
|
- `template/src/lib/objects/Item.ts` — example `@smrt()` object
|
|
18
|
+
- `template/src/routes/+page.server.ts` + `+page.svelte` — reference SSR data loading: server load queries collections directly (opt-in read cache via `cache: { ttl }`), declares `depends('smrt:items')`, and a form action + `invalidate('smrt:items')` demonstrates post-mutation refresh
|
|
17
19
|
- `template/src/app.d.ts` — `App.Locals` extends `SessionLocals` from `@happyvertical/smrt-users/sveltekit`
|
|
18
20
|
|
|
19
21
|
## Test Infrastructure
|
|
@@ -23,6 +25,7 @@ Base SvelteKit project template used by `smrt init`. Scaffolds a full-stack, mul
|
|
|
23
25
|
|
|
24
26
|
## Key Patterns
|
|
25
27
|
|
|
28
|
+
- **SSR data loading convention**: reference pages load collection data in `+page.server.ts` (serialized into the HTML, hydrated without a duplicate client fetch). Loads declare `depends('smrt:<collection>')` (REST route segment naming: `/api/items` → `smrt:items`); mutations call `invalidate('smrt:<collection>')` to re-run them.
|
|
26
29
|
- **Pluggable tenant resolver**: `tenancy.ts` exports strategy functions + a `createTenantResolver()` factory. Consumers swap strategies by editing one line.
|
|
27
30
|
- **File copying with placeholder substitution**: project name is replaced in template files during generation.
|
|
28
31
|
- **No template-internal test pollution**: `__tests__/` and the `.svelte-kit/` stub are package-level and excluded from `copyTemplate` output.
|
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ SvelteKit project template with SMRT framework integration. Scaffolds a full-sta
|
|
|
7
7
|
- SvelteKit 2.x with Svelte 5 and TypeScript
|
|
8
8
|
- `smrtPlugin()` Vite integration for automatic REST API route generation
|
|
9
9
|
- Example `@smrt()` object (`Item.ts`) with barrel export
|
|
10
|
+
- Reference SSR data loading: `+page.server.ts` server load with the `depends('smrt:<collection>')` / `invalidate('smrt:<collection>')` refresh convention and opt-in collection read caching
|
|
10
11
|
- Server-side SMRT initialization (`src/lib/server/smrt.ts`)
|
|
11
12
|
- `smrt.config.ts` with SQLite database and optional AI provider
|
|
12
13
|
- `.env.example` with starter environment variables
|
|
@@ -81,9 +82,11 @@ template/
|
|
|
81
82
|
│ │ ├── index.ts # Barrel export
|
|
82
83
|
│ │ └── Item.ts # Example @smrt() object
|
|
83
84
|
│ └── server/
|
|
84
|
-
│ ├── smrt.ts # Server-side SMRT initialization
|
|
85
|
+
│ ├── smrt.ts # Server-side SMRT initialization + manifest seeding
|
|
86
|
+
│ ├── smrt-register.ts # Object registration (generated by smrtPlugin, gitignored)
|
|
85
87
|
│ └── tenancy.ts # Pluggable tenant resolver
|
|
86
88
|
└── routes/
|
|
89
|
+
├── +page.server.ts # Home page server load + demo mutation (reference pattern)
|
|
87
90
|
└── +page.svelte # Home page
|
|
88
91
|
```
|
|
89
92
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-template-sveltekit",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.8",
|
|
4
4
|
"description": "SvelteKit project template with SMRT framework integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"directory": "packages/template-sveltekit"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
|
-
"@happyvertical/smrt-core": "0.37.
|
|
32
|
+
"@happyvertical/smrt-core": "0.37.8"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"vitest": "^4.1.9"
|
package/template/AGENTS.md
CHANGED
|
@@ -16,6 +16,15 @@ tools, and agent/developer knowledge artifacts.
|
|
|
16
16
|
|
|
17
17
|
## Package Guidance
|
|
18
18
|
|
|
19
|
+
- Load page data in `+page.server.ts` server loads that query collections
|
|
20
|
+
directly (see `src/routes/+page.server.ts`); never fetch `/api/*` from
|
|
21
|
+
`$effect`/`onMount` for initial page data.
|
|
22
|
+
- Declare `depends('smrt:<collection>')` in loads (route segment naming:
|
|
23
|
+
`/api/items` → `smrt:items`) and call `invalidate('smrt:<collection>')`
|
|
24
|
+
after mutations to refresh in place.
|
|
25
|
+
- Opt read-heavy SSR reads into the collection cache with
|
|
26
|
+
`list({ cache: { ttl } })`; skip it for per-user data and admin editors
|
|
27
|
+
(see README "Data loading").
|
|
19
28
|
- Keep SMRT object relationship metadata close to the `@smrt()` decorator.
|
|
20
29
|
- Use `knowledge: false` only for objects that should stay out of authored
|
|
21
30
|
agent context while remaining in the runtime manifest.
|
package/template/README.md
CHANGED
|
@@ -34,16 +34,137 @@ A SvelteKit application with SMRT framework integration for rapid development of
|
|
|
34
34
|
│ │ │ ├── index.ts # Export all objects here
|
|
35
35
|
│ │ │ └── Item.ts # Example SMRT object
|
|
36
36
|
│ │ └── server/
|
|
37
|
-
│ │ ├── smrt.ts # SMRT configuration
|
|
37
|
+
│ │ ├── smrt.ts # SMRT configuration + local manifest seeding
|
|
38
|
+
│ │ ├── smrt-register.ts # Object registration (generated by smrtPlugin, gitignored)
|
|
38
39
|
│ │ └── tenancy.ts # Pluggable tenant resolver
|
|
39
40
|
│ └── routes/
|
|
40
41
|
│ ├── api/ # Auto-generated API routes (don't edit!)
|
|
42
|
+
│ ├── +page.server.ts # Home page server load + demo mutation (reference pattern)
|
|
41
43
|
│ └── +page.svelte # Home page
|
|
42
44
|
├── smrt.config.ts # Root SMRT config
|
|
43
45
|
├── vite.config.ts # Vite + SMRT plugin config
|
|
44
46
|
└── svelte.config.js # SvelteKit config
|
|
45
47
|
```
|
|
46
48
|
|
|
49
|
+
## Data loading (SSR + hydration)
|
|
50
|
+
|
|
51
|
+
The home page (`src/routes/+page.server.ts` + `+page.svelte`) is the reference
|
|
52
|
+
pattern for getting collection data onto a page. Copy it for your own routes.
|
|
53
|
+
|
|
54
|
+
**Query collections in a server `load`, not with `fetch` on mount.** A server
|
|
55
|
+
load runs during SSR, reads the database directly through the collection API
|
|
56
|
+
(no HTTP round-trip through your own `/api/*` routes), and SvelteKit
|
|
57
|
+
serializes the result into the initial HTML. The client hydrates that data
|
|
58
|
+
without re-fetching — the page renders with its data on first paint, and the
|
|
59
|
+
first client render issues no duplicate request.
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// src/routes/+page.server.ts
|
|
63
|
+
import type { Item } from '$lib/objects/Item';
|
|
64
|
+
import { getCollection } from '$lib/server/smrt';
|
|
65
|
+
import type { PageServerLoad } from './$types';
|
|
66
|
+
|
|
67
|
+
export const load: PageServerLoad = async ({ depends }) => {
|
|
68
|
+
depends('smrt:items'); // see "Refreshing after mutations" below
|
|
69
|
+
|
|
70
|
+
const items = await getCollection<Item>('Item');
|
|
71
|
+
const rows = await items.list({ orderBy: 'created_at DESC', limit: 50 });
|
|
72
|
+
|
|
73
|
+
// Return plain serializable objects, not class instances.
|
|
74
|
+
return {
|
|
75
|
+
items: rows.map((item) => ({ id: item.id, title: item.title, status: item.status })),
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```svelte
|
|
81
|
+
<!-- src/routes/+page.svelte -->
|
|
82
|
+
<script lang="ts">
|
|
83
|
+
import type { PageProps } from './$types';
|
|
84
|
+
|
|
85
|
+
let { data }: PageProps = $props();
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
{#each data.items as item (item.id)}
|
|
89
|
+
<p>{item.title}</p>
|
|
90
|
+
{/each}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Avoid the mount-time fetch anti-pattern (`$effect(() => { fetch('/api/items') ... })`):
|
|
94
|
+
it renders an empty page first, waterfalls a second request on every
|
|
95
|
+
navigation, and skips SvelteKit's load/hydration machinery entirely. Reserve
|
|
96
|
+
client-side `fetch` of the generated `/api/*` routes for after-load
|
|
97
|
+
interactions (mutations, polling, external clients) — not for initial page
|
|
98
|
+
data.
|
|
99
|
+
|
|
100
|
+
### Refreshing after mutations (`depends` / `invalidate`)
|
|
101
|
+
|
|
102
|
+
Convention: each load declares one dependency key per collection it reads,
|
|
103
|
+
named `smrt:<collection>` — where `<collection>` is the generated REST route
|
|
104
|
+
segment (`/api/items` → `smrt:items`). After a mutation, client code calls
|
|
105
|
+
`invalidate('smrt:<collection>')` and every load that declared that key
|
|
106
|
+
re-runs, updating the page in place:
|
|
107
|
+
|
|
108
|
+
```svelte
|
|
109
|
+
<script lang="ts">
|
|
110
|
+
import { invalidate } from '$app/navigation';
|
|
111
|
+
|
|
112
|
+
async function createItem(title: string) {
|
|
113
|
+
await fetch('/api/items', {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: { 'content-type': 'application/json' },
|
|
116
|
+
body: JSON.stringify({ title }),
|
|
117
|
+
});
|
|
118
|
+
await invalidate('smrt:items'); // re-runs every load that declared depends('smrt:items')
|
|
119
|
+
}
|
|
120
|
+
</script>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The home page demonstrates the same flow with a progressively-enhanced form
|
|
124
|
+
action: `use:enhance` applies the action result with
|
|
125
|
+
`update({ invalidateAll: false })`, then calls `invalidate('smrt:items')` so
|
|
126
|
+
only the loads that depend on items re-run (instead of SvelteKit's default
|
|
127
|
+
invalidate-everything). Note that the generated `/api/*` routes are
|
|
128
|
+
fail-closed — mutations 401 unless the request is authenticated or the object
|
|
129
|
+
opts in via `@smrt({ api: { public: true } })` — which is why the demo uses a
|
|
130
|
+
form action running server-side.
|
|
131
|
+
|
|
132
|
+
### Caching SSR reads
|
|
133
|
+
|
|
134
|
+
Server-side reads can opt into the collection read cache, per call or per
|
|
135
|
+
model:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// Per call — this load only
|
|
139
|
+
const rows = await items.list({ where: { status: 'published' }, cache: { ttl: 60_000 } });
|
|
140
|
+
|
|
141
|
+
// Per model — every read of this collection
|
|
142
|
+
@smrt({ cache: { ttl: 60_000 } })
|
|
143
|
+
export class Item extends SmrtObject { /* ... */ }
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Cached rows are keyed by the final SQL + parameters and live for `ttl`
|
|
147
|
+
milliseconds. Every mutation that goes through SMRT (`create()`, `save()`,
|
|
148
|
+
`delete()`) invalidates the table's cached entries in this process
|
|
149
|
+
automatically — so the post-mutation `invalidate('smrt:...')` re-run sees
|
|
150
|
+
fresh rows, as the home page demonstrates.
|
|
151
|
+
|
|
152
|
+
Use it for read-heavy, write-rare data rendered for many visitors: catalogs,
|
|
153
|
+
published content, navigation. Do **not** cache:
|
|
154
|
+
|
|
155
|
+
- **Per-user data** (dashboards, "my items"): each user's query shape churns
|
|
156
|
+
the cache for little reuse, and any staleness is personally visible.
|
|
157
|
+
- **Admin editors / read-your-writes flows**: editors must always see the
|
|
158
|
+
row they just changed, including across processes.
|
|
159
|
+
- **Multi-replica deployments without `crossProcess: true`**: the cache is
|
|
160
|
+
per-process, so a write on one replica leaves the others stale until TTL.
|
|
161
|
+
Set `cache: { ttl, crossProcess: true }` (uses the database adapter's
|
|
162
|
+
notification capability, e.g. Postgres LISTEN/NOTIFY) or keep the TTL
|
|
163
|
+
short enough to tolerate.
|
|
164
|
+
|
|
165
|
+
Pass `cache: false` on a specific call to bypass a model-level cache where
|
|
166
|
+
freshness matters.
|
|
167
|
+
|
|
47
168
|
## Multi-tenancy
|
|
48
169
|
|
|
49
170
|
This template ships with multi-tenancy pre-wired. Out of the box you get:
|
package/template/package.json
CHANGED
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"vite": "^8.1.2"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@happyvertical/smrt-core": "^0.37.
|
|
24
|
-
"@happyvertical/smrt-tenancy": "^0.37.
|
|
25
|
-
"@happyvertical/smrt-users": "^0.37.
|
|
23
|
+
"@happyvertical/smrt-core": "^0.37.8",
|
|
24
|
+
"@happyvertical/smrt-tenancy": "^0.37.8",
|
|
25
|
+
"@happyvertical/smrt-users": "^0.37.8"
|
|
26
26
|
}
|
|
27
27
|
}
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { sequence } from '@sveltejs/kit/hooks';
|
|
28
|
-
import type { Handle
|
|
28
|
+
import type { Handle } from '@sveltejs/kit';
|
|
29
29
|
|
|
30
30
|
import { createSessionHandler } from '@happyvertical/smrt-users/sveltekit';
|
|
31
31
|
import {
|
|
@@ -59,9 +59,10 @@ enableTenancy();
|
|
|
59
59
|
*/
|
|
60
60
|
const tenancyHandle = createSvelteKitHandle({
|
|
61
61
|
resolveTenantId: async (event) => {
|
|
62
|
-
// The createSvelteKitHandle adapter passes a structural event
|
|
63
|
-
//
|
|
64
|
-
|
|
62
|
+
// The createSvelteKitHandle adapter passes a structural event that is
|
|
63
|
+
// directly assignable to the resolver's `TenantResolverEvent` shape
|
|
64
|
+
// (`url` + `request.headers`), so no cast is needed.
|
|
65
|
+
const result = await resolveTenant(event);
|
|
65
66
|
return result.tenantId;
|
|
66
67
|
},
|
|
67
68
|
}) as unknown as Handle;
|
|
@@ -5,11 +5,41 @@
|
|
|
5
5
|
* Import this file in your routes to get properly configured collections.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
import {
|
|
12
|
+
ObjectRegistry,
|
|
13
|
+
type SmrtClassOptions,
|
|
14
|
+
type SmrtObject,
|
|
15
|
+
} from '@happyvertical/smrt-core';
|
|
16
|
+
import { loadManifestFromPathSync } from '@happyvertical/smrt-core/manifest';
|
|
17
|
+
|
|
18
|
+
// Import all SMRT objects to register them.
|
|
11
19
|
import '../objects/index.js';
|
|
12
20
|
|
|
21
|
+
// Upgrade those registrations to package-qualified identities
|
|
22
|
+
// (`<package>:<Class>`) — required to link each class to its scanned field
|
|
23
|
+
// metadata below. smrtPlugin() generates this module on every dev/build run
|
|
24
|
+
// (it is gitignored, like the generated API routes); guard the import so a
|
|
25
|
+
// fresh checkout that has not run the plugin yet still boots.
|
|
26
|
+
try {
|
|
27
|
+
await import('./smrt-register.js');
|
|
28
|
+
} catch {
|
|
29
|
+
// Not generated yet — the first `vite dev` / `vite build` will create it.
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Hydrate field metadata for this app's own objects from the local manifest
|
|
33
|
+
// written by smrtPlugin() (`.smrt/manifest.json`). The decorator import above
|
|
34
|
+
// registers the classes, but their scanned field metadata (plain properties
|
|
35
|
+
// like `title: string = ''`) lives in the manifest — without seeding it, the
|
|
36
|
+
// runtime only knows the framework base fields and server-side writes would
|
|
37
|
+
// silently drop your domain columns.
|
|
38
|
+
const localManifestPath = join(process.cwd(), '.smrt', 'manifest.json');
|
|
39
|
+
if (existsSync(localManifestPath)) {
|
|
40
|
+
loadManifestFromPathSync(localManifestPath);
|
|
41
|
+
}
|
|
42
|
+
|
|
13
43
|
declare global {
|
|
14
44
|
// eslint-disable-next-line no-var
|
|
15
45
|
var __smrtGetRequestScopedDatabase:
|
|
@@ -67,7 +97,7 @@ export function getSmrtConfig(className: string): SmrtClassOptions {
|
|
|
67
97
|
* const products = await getCollection<Product>('Product');
|
|
68
98
|
* const items = await products.list();
|
|
69
99
|
*/
|
|
70
|
-
export async function getCollection<T>(className: string) {
|
|
100
|
+
export async function getCollection<T extends SmrtObject>(className: string) {
|
|
71
101
|
const config = getSmrtConfig(className);
|
|
72
102
|
const objectOverride = objectOverrides[className];
|
|
73
103
|
const requestScopedDb = objectOverride?.db
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Home page server load + demo mutation.
|
|
3
|
+
*
|
|
4
|
+
* This file is the reference data-loading pattern for SMRT + SvelteKit apps:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Query collections in a server `load`** — not with a client-side
|
|
7
|
+
* `fetch` on mount. SvelteKit runs this during SSR, serializes the
|
|
8
|
+
* returned data into the initial HTML, and hydrates it on the client
|
|
9
|
+
* without re-fetching. The page renders with its data on first paint;
|
|
10
|
+
* there is no empty-then-fetch waterfall and no duplicate first-render
|
|
11
|
+
* request. Server loads read the database directly through the
|
|
12
|
+
* collection API — they do not round-trip through your own
|
|
13
|
+
* `/api/*` routes.
|
|
14
|
+
*
|
|
15
|
+
* 2. **Declare one dependency key per collection** with
|
|
16
|
+
* `depends('smrt:<collection>')`, where `<collection>` matches the
|
|
17
|
+
* generated REST route segment (`/api/items` → `smrt:items`). After a
|
|
18
|
+
* mutation, client code calls `invalidate('smrt:<collection>')`
|
|
19
|
+
* (see `+page.svelte`) and every load that declared the key re-runs.
|
|
20
|
+
*
|
|
21
|
+
* 3. **Opt read-heavy SSR reads into the collection read cache** with
|
|
22
|
+
* `cache: { ttl }`. Mutations that go through SMRT (`create()`,
|
|
23
|
+
* `save()`, `delete()`) invalidate the table's cache entries
|
|
24
|
+
* in-process automatically, so the post-mutation `invalidate()`
|
|
25
|
+
* re-runs this load against fresh rows. See "Data loading" in the
|
|
26
|
+
* README for multi-replica caveats and when NOT to cache.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { fail } from '@sveltejs/kit';
|
|
30
|
+
import type { Item } from '$lib/objects/Item';
|
|
31
|
+
import { getCollection } from '$lib/server/smrt';
|
|
32
|
+
import type { Actions, PageServerLoad } from './$types';
|
|
33
|
+
|
|
34
|
+
export const load: PageServerLoad = async ({ depends }) => {
|
|
35
|
+
// Convention: `smrt:<collection>`. Registered before the query so even a
|
|
36
|
+
// failed load (e.g. database not initialized yet) re-runs on invalidation.
|
|
37
|
+
depends('smrt:items');
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const items = await getCollection<Item>('Item');
|
|
41
|
+
const rows = await items.list({
|
|
42
|
+
orderBy: 'created_at DESC',
|
|
43
|
+
limit: 50,
|
|
44
|
+
// Opt this SSR read into the collection read cache (issue #1499).
|
|
45
|
+
// Safe here because all writes go through SMRT, which invalidates the
|
|
46
|
+
// table's entries in this process. Running multiple replicas? Add
|
|
47
|
+
// `crossProcess: true` or drop the cache — see the README.
|
|
48
|
+
cache: { ttl: 30_000 },
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
// Return plain serializable objects: SvelteKit encodes load data into
|
|
53
|
+
// the initial HTML with devalue, so pick the fields the page needs
|
|
54
|
+
// rather than returning class instances. Persisted rows always carry a
|
|
55
|
+
// UUID `id`; the filter narrows the field's `string | null | undefined`
|
|
56
|
+
// type instead of coercing — a '' fallback would produce colliding
|
|
57
|
+
// `{#each}` keys if an id were ever missing.
|
|
58
|
+
items: rows
|
|
59
|
+
.filter((item): item is Item & { id: string } => Boolean(item.id))
|
|
60
|
+
.map((item) => ({
|
|
61
|
+
id: item.id,
|
|
62
|
+
title: item.title,
|
|
63
|
+
status: item.status,
|
|
64
|
+
})),
|
|
65
|
+
loadError: null as string | null,
|
|
66
|
+
};
|
|
67
|
+
} catch (e) {
|
|
68
|
+
// First run before `smrt db:setup` has created the tables: render the
|
|
69
|
+
// page with guidance instead of a 500.
|
|
70
|
+
return {
|
|
71
|
+
items: [] as { id: string; title: string; status: string }[],
|
|
72
|
+
loadError: e instanceof Error ? e.message : 'Failed to load items',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const actions: Actions = {
|
|
78
|
+
/**
|
|
79
|
+
* Demo mutation for the invalidation convention: creates an Item, then the
|
|
80
|
+
* client (`+page.svelte`) calls `invalidate('smrt:items')` so the load
|
|
81
|
+
* above re-runs and the new row appears without a full page reload.
|
|
82
|
+
*
|
|
83
|
+
* NOTE: this demo action is intentionally unauthenticated so the refresh
|
|
84
|
+
* flow works out of the box. Real mutations should be gated on
|
|
85
|
+
* `locals.user` / permissions (the generated `/api/*` routes are already
|
|
86
|
+
* fail-closed: they 401 unless authenticated or `@smrt({ api: { public } })`).
|
|
87
|
+
*/
|
|
88
|
+
create: async ({ request }) => {
|
|
89
|
+
const form = await request.formData();
|
|
90
|
+
const title = String(form.get('title') ?? '').trim();
|
|
91
|
+
|
|
92
|
+
if (!title) {
|
|
93
|
+
return fail(400, { title, error: 'Title is required' });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const items = await getCollection<Item>('Item');
|
|
98
|
+
await items.create({ title });
|
|
99
|
+
return { created: title };
|
|
100
|
+
} catch (e) {
|
|
101
|
+
return fail(500, {
|
|
102
|
+
title,
|
|
103
|
+
error: e instanceof Error ? e.message : 'Failed to create item',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
};
|
|
@@ -1,23 +1,15 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const data = await response.json();
|
|
14
|
-
items = data.items || [];
|
|
15
|
-
} catch (e) {
|
|
16
|
-
error = e instanceof Error ? e.message : 'Failed to fetch items';
|
|
17
|
-
} finally {
|
|
18
|
-
loading = false;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
2
|
+
import { enhance } from '$app/forms';
|
|
3
|
+
import { invalidate } from '$app/navigation';
|
|
4
|
+
import type { PageProps } from './$types';
|
|
5
|
+
|
|
6
|
+
// `data` comes from the server load in `+page.server.ts`. It is fetched
|
|
7
|
+
// during SSR, serialized into the initial HTML, and hydrated here — the
|
|
8
|
+
// first client render issues NO fetch for it. It re-runs only when
|
|
9
|
+
// something calls `invalidate('smrt:items')` (below) or on navigation.
|
|
10
|
+
let { data, form }: PageProps = $props();
|
|
11
|
+
|
|
12
|
+
let creating = $state(false);
|
|
21
13
|
</script>
|
|
22
14
|
|
|
23
15
|
<svelte:head>
|
|
@@ -30,15 +22,14 @@
|
|
|
30
22
|
<section>
|
|
31
23
|
<h2>Items</h2>
|
|
32
24
|
|
|
33
|
-
{#if
|
|
34
|
-
<p>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
<p>No items yet. Create your first item!</p>
|
|
25
|
+
{#if data.loadError}
|
|
26
|
+
<p class="error">{data.loadError}</p>
|
|
27
|
+
<p>If this is a fresh project, initialize the database with <code>smrt db:setup</code>.</p>
|
|
28
|
+
{:else if data.items.length === 0}
|
|
29
|
+
<p>No items yet. Create your first item below!</p>
|
|
39
30
|
{:else}
|
|
40
31
|
<ul>
|
|
41
|
-
{#each items as item}
|
|
32
|
+
{#each data.items as item (item.id)}
|
|
42
33
|
<li>
|
|
43
34
|
<strong>{item.title}</strong>
|
|
44
35
|
<span class="status">{item.status}</span>
|
|
@@ -46,12 +37,58 @@
|
|
|
46
37
|
{/each}
|
|
47
38
|
</ul>
|
|
48
39
|
{/if}
|
|
40
|
+
|
|
41
|
+
<!--
|
|
42
|
+
Post-mutation refresh convention: submit the mutation (a form action
|
|
43
|
+
here, but a fetch to a generated /api route works the same way), then
|
|
44
|
+
call `invalidate('smrt:items')`. Every load that declared
|
|
45
|
+
`depends('smrt:items')` re-runs and the list updates in place.
|
|
46
|
+
Without JavaScript this form still works — SvelteKit falls back to a
|
|
47
|
+
full-page POST + re-render.
|
|
48
|
+
-->
|
|
49
|
+
<form
|
|
50
|
+
method="POST"
|
|
51
|
+
action="?/create"
|
|
52
|
+
use:enhance={() => {
|
|
53
|
+
creating = true;
|
|
54
|
+
return async ({ result, update }) => {
|
|
55
|
+
try {
|
|
56
|
+
// Apply the action result (sets `form`, resets the input) but
|
|
57
|
+
// skip SvelteKit's default invalidateAll() — we re-run only the
|
|
58
|
+
// loads that depend on this collection.
|
|
59
|
+
await update({ invalidateAll: false });
|
|
60
|
+
if (result.type === 'success') {
|
|
61
|
+
await invalidate('smrt:items');
|
|
62
|
+
}
|
|
63
|
+
} finally {
|
|
64
|
+
// Always re-enable the form, even if update()/invalidate()
|
|
65
|
+
// throws (network error, aborted navigation, …).
|
|
66
|
+
creating = false;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<input
|
|
72
|
+
name="title"
|
|
73
|
+
placeholder="New item title"
|
|
74
|
+
aria-label="New item title"
|
|
75
|
+
required
|
|
76
|
+
/>
|
|
77
|
+
<button type="submit" disabled={creating}>
|
|
78
|
+
{creating ? 'Creating…' : 'Create item'}
|
|
79
|
+
</button>
|
|
80
|
+
</form>
|
|
81
|
+
|
|
82
|
+
{#if form?.error}
|
|
83
|
+
<p class="error">{form.error}</p>
|
|
84
|
+
{/if}
|
|
49
85
|
</section>
|
|
50
86
|
|
|
51
87
|
<section>
|
|
52
88
|
<h2>Getting Started</h2>
|
|
53
89
|
<ul>
|
|
54
90
|
<li>Edit <code>src/lib/objects/Item.ts</code> to customize your SMRT object</li>
|
|
91
|
+
<li>Load data in <code>+page.server.ts</code> files — see this page's load for the SSR + <code>depends</code>/<code>invalidate</code> pattern</li>
|
|
55
92
|
<li>Run <code>smrt objects</code> to see registered objects</li>
|
|
56
93
|
<li>Run <code>smrt generate-routes</code> to regenerate API routes</li>
|
|
57
94
|
<li>API routes are auto-generated in <code>src/routes/api/</code></li>
|
|
@@ -90,6 +127,35 @@
|
|
|
90
127
|
margin-left: 8px;
|
|
91
128
|
}
|
|
92
129
|
|
|
130
|
+
form {
|
|
131
|
+
display: flex;
|
|
132
|
+
gap: 0.5rem;
|
|
133
|
+
margin-top: 1rem;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
input {
|
|
137
|
+
flex: 1;
|
|
138
|
+
padding: 0.5rem;
|
|
139
|
+
border: 1px solid #ccc;
|
|
140
|
+
border-radius: var(--smrt-radius-sm, 4px);
|
|
141
|
+
font-size: 1em;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
button {
|
|
145
|
+
padding: 0.5rem 1rem;
|
|
146
|
+
border: none;
|
|
147
|
+
border-radius: var(--smrt-radius-sm, 4px);
|
|
148
|
+
background: #667eea;
|
|
149
|
+
color: white;
|
|
150
|
+
font-size: 1em;
|
|
151
|
+
cursor: pointer;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
button:disabled {
|
|
155
|
+
opacity: 0.6;
|
|
156
|
+
cursor: wait;
|
|
157
|
+
}
|
|
158
|
+
|
|
93
159
|
code {
|
|
94
160
|
background: #e0e0e0;
|
|
95
161
|
padding: 2px 6px;
|
package/template/vite.config.ts
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import { sveltekit } from '@sveltejs/kit/vite';
|
|
2
|
+
import { smrtConsumer } from '@happyvertical/smrt-core/consumer-plugin';
|
|
2
3
|
import { smrtPlugin } from '@happyvertical/smrt-core/vite-plugin';
|
|
3
4
|
import { defineConfig } from 'vite';
|
|
4
5
|
|
|
5
6
|
export default defineConfig({
|
|
7
|
+
// Lower `@smrt()` legacy (experimentalDecorators) decorators explicitly.
|
|
8
|
+
// tsconfig.json sets experimentalDecorators, but Vite's oxc transform does
|
|
9
|
+
// not reliably honor it through the SvelteKit `extends
|
|
10
|
+
// "./.svelte-kit/tsconfig.json"` chain — raw `@decorator class` syntax then
|
|
11
|
+
// reaches the SSR runtime and throws `SyntaxError: Invalid or unexpected
|
|
12
|
+
// token` on the first request.
|
|
13
|
+
oxc: {
|
|
14
|
+
decorator: {
|
|
15
|
+
legacy: true,
|
|
16
|
+
emitDecoratorMetadata: true,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
6
19
|
plugins: [
|
|
7
20
|
sveltekit(),
|
|
8
21
|
smrtPlugin({
|
|
@@ -17,5 +30,9 @@ export default defineConfig({
|
|
|
17
30
|
configFileName: 'smrt.ts',
|
|
18
31
|
},
|
|
19
32
|
}),
|
|
33
|
+
// Required alongside smrtPlugin(): this project depends on external SMRT
|
|
34
|
+
// packages (@happyvertical/smrt-users, smrt-tenancy, …), and the consumer
|
|
35
|
+
// plugin emits `.smrt/register.js` so their classes load for CLI/runtime.
|
|
36
|
+
smrtConsumer(),
|
|
20
37
|
],
|
|
21
38
|
});
|