@webjsdev/mcp 0.1.7 → 0.1.9

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 (37) hide show
  1. package/README.md +1 -1
  2. package/package.json +4 -1
  3. package/resources/AGENTS.md +86 -72
  4. package/resources/SKILL.md +226 -0
  5. package/resources/references/auth-and-sessions.md +220 -0
  6. package/resources/references/built-ins.md +200 -0
  7. package/resources/references/client-router-and-streaming.md +204 -0
  8. package/resources/references/components.md +167 -0
  9. package/resources/references/data-and-actions.md +187 -0
  10. package/resources/references/muscle-memory-gotchas.md +170 -0
  11. package/resources/references/optimistic-ui.md +128 -0
  12. package/resources/references/routing-and-pages.md +158 -0
  13. package/resources/references/runtime.md +80 -0
  14. package/resources/references/service-worker.md +78 -0
  15. package/resources/references/styling.md +123 -0
  16. package/resources/references/testing.md +125 -0
  17. package/resources/references/typescript.md +148 -0
  18. package/src/mcp-docs.js +27 -14
  19. package/src/mcp-source.js +1 -1
  20. package/src/mcp.js +9 -47
  21. package/src/routes-report.js +90 -0
  22. package/resources/agent-docs/advanced.md +0 -1334
  23. package/resources/agent-docs/built-ins.md +0 -389
  24. package/resources/agent-docs/components-shadowing.md +0 -87
  25. package/resources/agent-docs/components.md +0 -734
  26. package/resources/agent-docs/configuration.md +0 -284
  27. package/resources/agent-docs/framework-dev.md +0 -73
  28. package/resources/agent-docs/lit-muscle-memory-gotchas.md +0 -470
  29. package/resources/agent-docs/metadata.md +0 -334
  30. package/resources/agent-docs/orm.md +0 -198
  31. package/resources/agent-docs/recipes.md +0 -664
  32. package/resources/agent-docs/runtime.md +0 -65
  33. package/resources/agent-docs/service-worker.md +0 -104
  34. package/resources/agent-docs/styling.md +0 -279
  35. package/resources/agent-docs/testing.md +0 -479
  36. package/resources/agent-docs/types-and-mutations.md +0 -171
  37. package/resources/agent-docs/typescript.md +0 -402
@@ -1,334 +0,0 @@
1
- # Metadata API: full field reference
2
-
3
- Page modules export `metadata` (static) or `generateMetadata(ctx)`
4
- (request-scoped). Values flow into `<head>` at SSR time and merge across
5
- nested layouts (deeper wins). Surface is Next.js-compatible.
6
-
7
- ## Type it with `Metadata`
8
-
9
- webjs exports a `Metadata` type covering every field below, so a typo
10
- (`titel`, `descripton`), wrong nesting, or a wrong-typed value
11
- (`themeColor: 123`) is a tsserver / checkJs error instead of a silently
12
- dropped tag. Import it from `@webjsdev/core` (the same isomorphic surface
13
- a page already imports `html` from). `MetadataContext` types the
14
- `generateMetadata(ctx)` argument.
15
-
16
- ```ts
17
- import type { Metadata, MetadataContext } from '@webjsdev/core';
18
-
19
- // static
20
- export const metadata: Metadata = { title: 'Home', description: 'Welcome' };
21
-
22
- // request-scoped
23
- export async function generateMetadata(ctx: MetadataContext): Promise<Metadata> {
24
- return { title: `Post: ${ctx.params.slug}` };
25
- }
26
- ```
27
-
28
- Every field is optional. Where the framework accepts a string OR an
29
- object (`title`, `viewport`, `robots`, `appleWebApp`, `icons`), the type
30
- is a union, so both forms type-check. The type lives in
31
- `packages/core/src/metadata.d.ts` (types-only, zero runtime, no build);
32
- it mirrors exactly what `packages/server/src/ssr.js` consumes.
33
-
34
- ```ts
35
- export const metadata = {
36
- // ----- Identity -----
37
- title: 'Blog post title', // → <title>
38
- // OR { template, default, absolute }. The template propagates from
39
- // outer layouts, and deeper plain-string titles get wrapped via "%s".
40
- // title: { template: '%s | webjs', default: 'webjs', absolute: 'standalone title' },
41
- description: 'Short summary', // → <meta name="description">
42
- keywords: ['ai', 'web components'], // → <meta name="keywords"> (or single string)
43
- authors: [{ name: 'Vivek', url: 'https://...' }], // → <meta name="author"> (+ optional <link rel="author">)
44
- creator: 'Vivek', // → <meta name="creator">
45
- publisher: 'My Co', // → <meta name="publisher">
46
- applicationName: 'webjs', // → <meta name="application-name">
47
- generator: 'webjs 0.5', // → <meta name="generator">
48
- referrer: 'origin-when-cross-origin', // → <meta name="referrer">
49
-
50
- // ----- Base URL for relative metadata URLs -----
51
- metadataBase: 'https://example.com',
52
- // Any relative URL in openGraph.image, openGraph.url, twitter.image,
53
- // alternates.canonical / languages / media / types, icons, authors[].url,
54
- // archives / assets / bookmarks gets resolved against this base.
55
-
56
- // ----- Viewport / theme -----
57
- viewport: 'width=device-width,initial-scale=1', // string OR object
58
- // viewport: { width: 'device-width', initialScale: 1, maximumScale: 5, userScalable: true },
59
- // (or split-export: `export const viewport = { … }`, the Next.js 14+ style)
60
- themeColor: '#1c1613', // → <meta name="theme-color">
61
- colorScheme: 'light dark', // → <meta name="color-scheme">
62
-
63
- // ----- Crawler / SEO -----
64
- robots: { index: true, follow: true, googleBot: 'index, max-snippet:-1' },
65
- // OR robots: 'noindex, nofollow'
66
- alternates: {
67
- canonical: '/post', // → <link rel="canonical">
68
- languages: { 'es-ES': '/es', 'fr-FR': '/fr' }, // → hreflang <link>s
69
- media: { '(max-width: 600px)': '/mobile' }, // → media alternates
70
- types: { 'application/rss+xml': '/rss.xml' }, // → RSS / Atom alternates
71
- },
72
- verification: {
73
- google: 'token', // → <meta name="google-site-verification">
74
- yandex: 'token', // → <meta name="yandex-verification">
75
- yahoo: 'token', // → <meta name="y_key">
76
- me: 'https://me.example', // → <meta name="me"> (IndieAuth)
77
- other: { 'facebook-domain-verification': 'fb-token' },
78
- },
79
-
80
- // ----- Icons + manifest -----
81
- icons: {
82
- icon: [{ url: '/icon.svg', type: 'image/svg+xml' }, { url: '/icon-32.png', sizes: '32x32' }],
83
- apple: '/apple-touch-icon.png',
84
- shortcut: '/favicon.ico',
85
- other: [{ rel: 'mask-icon', url: '/mask.svg' }],
86
- },
87
- manifest: '/manifest.webmanifest', // → <link rel="manifest">
88
-
89
- // ----- Open Graph -----
90
- openGraph: {
91
- type: 'website',
92
- title: 'OG title',
93
- description: 'OG description',
94
- url: '/post', // relative resolves via metadataBase
95
- image: '/og.png', // ditto
96
- 'image:width': '1200',
97
- 'image:height': '630',
98
- 'image:alt': 'Post cover',
99
- 'site_name': 'My Site',
100
- },
101
-
102
- // ----- Twitter -----
103
- twitter: {
104
- card: 'summary_large_image', // required for big-image preview
105
- title: 'Twitter title',
106
- description: 'Twitter description',
107
- image: '/og.png',
108
- },
109
-
110
- // ----- iOS / mobile -----
111
- appleWebApp: {
112
- capable: true, // → apple-mobile-web-app-capable
113
- title: 'My App', // → apple-mobile-web-app-title
114
- statusBarStyle: 'black-translucent',
115
- startupImage: [{ url: '/splash.png', media: '(device-width: 320px)' }],
116
- },
117
- formatDetection: { telephone: false, email: false },
118
- itunes: { appId: '12345', appArgument: 'myapp://open' },
119
-
120
- // ----- Long-tail descriptive -----
121
- category: 'tech',
122
- classification: 'documentation',
123
- abstract: 'A short summary',
124
- archives: ['/archive-2024', '/archive-2023'], // → <link rel="archives">
125
- assets: '/assets-cdn', // → <link rel="assets">
126
- bookmarks: ['/bm-1'], // → <link rel="bookmark">
127
-
128
- // ----- Cache-control (response header, not <meta>) -----
129
- cacheControl: 'public, max-age=60', // pages default to no-store
130
- preload: [
131
- { href: '/public/fonts/Inter.woff2', as: 'font', type: 'font/woff2', crossorigin: 'anonymous' },
132
- ],
133
-
134
- // ----- Connection-warming hints (#243) -----
135
- preconnect: [ // → <link rel="preconnect">
136
- 'https://api.example.com', // warms DNS + TLS + TCP
137
- { url: 'https://fonts.gstatic.com', crossorigin: true },
138
- ],
139
- dnsPrefetch: 'https://analytics.example.com', // → <link rel="dns-prefetch"> (DNS only)
140
-
141
- // ----- Catch-all -----
142
- other: {
143
- 'msvalidate.01': 'bing-token',
144
- 'mobile-web-app-capable': 'yes',
145
- },
146
-
147
- // ----- JSON-LD structured data (schema.org) -----
148
- jsonLd: {
149
- '@context': 'https://schema.org',
150
- '@type': 'Article',
151
- headline: 'How webjs ships zero dead JS',
152
- author: { '@type': 'Person', name: 'Vivek' },
153
- datePublished: '2026-06-01',
154
- },
155
- };
156
- ```
157
-
158
- ## Request-scoped via `generateMetadata`
159
-
160
- ```ts
161
- import type { Metadata, MetadataContext } from '@webjsdev/core';
162
-
163
- export function generateMetadata(ctx: MetadataContext): Metadata {
164
- return {
165
- title: `Post: ${ctx.params.slug}`,
166
- metadataBase: new URL(ctx.url).origin,
167
- };
168
- }
169
- ```
170
-
171
- ## Split viewport export (Next.js 14+ pattern)
172
-
173
- All fields above under `metadata.viewport` are equally valid here, plus
174
- `themeColor` and `colorScheme` bubble up to their own meta tags.
175
-
176
- ```ts
177
- export const viewport = {
178
- width: 'device-width',
179
- initialScale: 1,
180
- themeColor: '#1c1613',
181
- colorScheme: 'light dark',
182
- };
183
- ```
184
-
185
- ## Special: `cacheControl`
186
-
187
- Emitted as a **response header**, not a `<meta>` tag. Pages default to
188
- `no-store` for safety. Opt into caching by setting this explicitly.
189
-
190
- A **public** value (e.g. `public, max-age=60`) also enables conditional
191
- GET on the page (#240): the buffered HTML response gets a weak content-hash
192
- `ETag` (`W/"..."`) and a repeat request whose `If-None-Match` matches it
193
- returns a `304 Not Modified` with no body. A `no-store` or `private` page
194
- gets NO ETag and never 304s, so private / per-user content is never
195
- revalidated across sessions. A streamed Suspense response is not ETagged.
196
- See the conditional-GET section in the framework root `AGENTS.md`.
197
-
198
- ## Connection-warming: `preconnect` / `dnsPrefetch` (#243)
199
-
200
- Warm a cross-origin connection the page is about to use (an API host, a
201
- font / image CDN) so the browser pays the DNS + TLS + TCP cost ahead of the
202
- first real request:
203
-
204
- ```ts
205
- export const metadata = {
206
- preconnect: [
207
- 'https://api.example.com', // bare URL
208
- { url: 'https://fonts.gstatic.com', crossorigin: true },// crossorigin set
209
- ],
210
- dnsPrefetch: 'https://analytics.example.com', // a single URL
211
- };
212
- ```
213
-
214
- - **`preconnect`** emits `<link rel="preconnect" href="..." [crossorigin]>`,
215
- warming DNS + TLS + TCP. Each entry is a URL string or
216
- `{ url, crossorigin? }` (`crossorigin: true` / `''` emits a bare
217
- `crossorigin`; a string like `'anonymous'` emits its value). A font CDN
218
- needs `crossorigin`.
219
- - **`dnsPrefetch`** emits `<link rel="dns-prefetch" href="...">`, which
220
- resolves DNS only (a lighter-weight precursor; it never carries
221
- `crossorigin`).
222
- - Each field takes a URL string, the object form, or an array of either.
223
- Every href is HTML-escaped.
224
-
225
- **Auto vendor preconnect.** For an UNPINNED app resolving vendors live from
226
- a cross-origin CDN, the framework auto-emits ONE
227
- `<link rel="preconnect" href="<cdn-origin>" crossorigin>` (the resolved
228
- vendor CDN origin, e.g. `https://ga.jspm.io`, derived from the importmap so
229
- a `--from jsdelivr` app preconnects to jsdelivr), so the browser warms that
230
- connection before the importmap resolves. It is DEDUPED against an
231
- author-declared `preconnect` to the same origin, and NONE is emitted for a
232
- same-origin pinned app (vendors served from the app's own origin) or an app
233
- with no cross-origin vendors.
234
-
235
- ## JSON-LD structured data (`jsonLd`)
236
-
237
- `metadata.jsonLd` emits schema.org structured data as one or more
238
- `<script type="application/ld+json">` blocks in `<head>`. This is the
239
- highest-leverage modern SEO surface (Google's Article, Product,
240
- BreadcrumbList, Organization, and FAQ rich results all read it). webjs
241
- stays true to its no-build identity here. JSON-LD is a web standard
242
- rendered as a plain script tag, so the framework ONLY serializes and
243
- escapes. There is no schema library and no validation. **You own the
244
- schema.org object.**
245
-
246
- **Single object** emits one script:
247
-
248
- ```ts
249
- import type { Metadata } from '@webjsdev/core';
250
-
251
- export const metadata: Metadata = {
252
- jsonLd: {
253
- '@context': 'https://schema.org',
254
- '@type': 'Article',
255
- headline: 'How webjs ships zero dead JS',
256
- author: { '@type': 'Person', name: 'Vivek' },
257
- datePublished: '2026-06-01',
258
- image: 'https://example.com/og.png',
259
- },
260
- };
261
- ```
262
-
263
- renders:
264
-
265
- ```html
266
- <script type="application/ld+json">{"@context":"https://schema.org","@type":"Article",...}</script>
267
- ```
268
-
269
- **An array** emits one script PER element. Use it to ship several graphs
270
- for one page (a Product alongside its BreadcrumbList, say):
271
-
272
- ```ts
273
- export const metadata: Metadata = {
274
- jsonLd: [
275
- {
276
- '@context': 'https://schema.org',
277
- '@type': 'Product',
278
- name: 'Acme Widget',
279
- offers: { '@type': 'Offer', price: '19.99', priceCurrency: 'USD' },
280
- },
281
- {
282
- '@context': 'https://schema.org',
283
- '@type': 'BreadcrumbList',
284
- itemListElement: [
285
- { '@type': 'ListItem', position: 1, name: 'Shop', item: 'https://example.com/shop' },
286
- { '@type': 'ListItem', position: 2, name: 'Widget', item: 'https://example.com/shop/widget' },
287
- ],
288
- },
289
- ],
290
- };
291
- ```
292
-
293
- **Per-request data** works the same way through `generateMetadata`, so a
294
- dynamic route can build the Article from the loaded record:
295
-
296
- ```ts
297
- import type { Metadata, MetadataContext } from '@webjsdev/core';
298
-
299
- export async function generateMetadata(ctx: MetadataContext): Promise<Metadata> {
300
- const post = await getPost(ctx.params.slug); // via a server query
301
- return {
302
- title: post.title,
303
- jsonLd: {
304
- '@context': 'https://schema.org',
305
- '@type': 'Article',
306
- headline: post.title,
307
- datePublished: post.publishedAt,
308
- author: { '@type': 'Person', name: post.authorName },
309
- },
310
- };
311
- }
312
- ```
313
-
314
- ### Escaping guarantee
315
-
316
- The serialized JSON is HTML-safe-escaped automatically. `<`, `>`, `&`,
317
- and the line separators U+2028 / U+2029 are replaced with their JSON
318
- Unicode escapes (`<` and friends). A JSON parser decodes those back to
319
- the original characters, so the embedded data still parses to your exact
320
- object, while the literal byte sequence `</script>` can never form in the
321
- served HTML. So a value containing `</script><img src=x onerror=...>`
322
- cannot break out of the script tag. You do not escape anything yourself.
323
-
324
- The block is a NON-EXECUTABLE data island (`type="application/ld+json"`),
325
- so a Content-Security-Policy `script-src` does not gate it and it carries
326
- NO nonce.
327
-
328
- ### Robustness
329
-
330
- The framework fails SAFE per element. An entry that is not a plain object,
331
- or an object with a circular reference that `JSON.stringify` cannot
332
- serialize, is skipped (with a one-line `console.warn`) and never breaks
333
- the rest of the head. Absent `jsonLd` emits nothing (the field is purely
334
- additive).
@@ -1,198 +0,0 @@
1
- # ORM: the Drizzle query surface (rc.3)
2
-
3
- The scaffold pins **`drizzle-orm@^1.0.0-rc.3`** (`db/columns.server.ts` header
4
- records this, research #562). rc.3 is the 1.0 release candidate with the new
5
- Relational Query Builder v2 (RQBv2) wired up in `db/connection.server.ts` via
6
- `drizzle({ client, relations })`. Several method overloads that older Drizzle
7
- tutorials rely on do NOT exist in rc.3, so training-data patterns from
8
- Drizzle 0.29 through 0.36 trip real TypeScript errors here. This page codifies
9
- the shapes that compile against the installed version.
10
-
11
- Ground rules for this version.
12
-
13
- 1. **Reads go through the relational query API** (`db.query.<table>.findFirst`
14
- / `.findMany`), not `db.select().from()`. Relations are pre-registered in
15
- `db/connection.server.ts`, so `with` joins work with zero boilerplate.
16
- 2. **`db.select()` takes no argument in rc.3.** The projection overload
17
- (`db.select({ ... })`) was removed. Passing a projection object is a
18
- `TS2554` compile error.
19
- 3. **`.returning()` takes no argument in rc.3.** It always returns the full
20
- inserted / updated / deleted rows (columns only, no relations). Passing a
21
- field object is a `TS2554` compile error.
22
-
23
- All examples assume the scaffold imports.
24
-
25
- ```ts
26
- import { eq, and, inArray, desc } from 'drizzle-orm';
27
- import { db } from '#db/connection.server.ts';
28
- import { posts, users } from '#db/schema.server.ts';
29
- ```
30
-
31
- These belong in a `*.server.ts` query or action (never a page or component,
32
- invariant 1). Reads live in `modules/<feature>/queries/*.server.ts`, mutations
33
- in `modules/<feature>/actions/*.server.ts`, one function per file.
34
-
35
- ---
36
-
37
- ## Reads: use the relational query API
38
-
39
- `db.query.<table>` reads a whole row by default, filters with a **plain object**
40
- `where` (the RQBv2 shape, not a `sql` expression), orders with an `orderBy`
41
- object, and pulls relations with `with`. This is the shape every scaffold query
42
- uses.
43
-
44
- ```ts
45
- // RIGHT: single row by a unique column
46
- const post = await db.query.posts.findFirst({
47
- where: { slug: input.slug },
48
- with: { author: { columns: { name: true, email: true } } },
49
- });
50
-
51
- // RIGHT: a list, newest first, with a projected column set
52
- const rows = await db.query.posts.findMany({
53
- orderBy: { createdAt: 'desc' },
54
- where: { authorId: me.id },
55
- columns: { id: true, slug: true, title: true },
56
- with: { author: { columns: { name: true } } },
57
- });
58
-
59
- // RIGHT: existence check, pull only the id
60
- const exists = await db.query.posts.findFirst({
61
- where: { slug },
62
- columns: { id: true },
63
- });
64
- ```
65
-
66
- The `where` object supports equality directly (`{ slug }`), nested operators
67
- (`{ createdAt: { gt: cutoff } }`), and `AND` by listing multiple keys. Reach for
68
- the imported `eq` / `and` / `inArray` operators only on the `db.select` /
69
- `db.delete` / `db.update` builders below, where the `.where()` clause is an SQL
70
- expression rather than the RQBv2 object.
71
-
72
- ### Pitfall: the `db.select({ ... })` projection overload is gone
73
-
74
- Older Drizzle let you project columns by passing an object to `select`. rc.3
75
- removed that overload, so the argument no longer type-checks.
76
-
77
- ```ts
78
- // WRONG (rc.3): TS2554 "Expected 0 arguments, but got 1"
79
- const rows = await db
80
- .select({ id: posts.id, title: posts.title })
81
- .from(posts);
82
- ```
83
-
84
- If you genuinely need the query-builder (a manual join, a `groupBy`, an
85
- aggregate the RQB does not express), call `select` with **no arguments** to get
86
- the full row, then narrow in JS.
87
-
88
- ```ts
89
- // RIGHT: full-row select, no projection argument
90
- const rows = await db.select().from(posts).where(eq(posts.authorId, me.id));
91
- ```
92
-
93
- For anything the relational API covers (by-id lookups, ordered lists, relation
94
- joins, column subsets), prefer `db.query.*`. It is the shorter, typed,
95
- relation-aware path and it sidesteps the removed overload entirely.
96
-
97
- ---
98
-
99
- ## Mutations: `insert` / `update` / `delete` with a no-arg `.returning()`
100
-
101
- Mutations use the query-builder, filter with the imported SQL operators, and
102
- read back rows with `.returning()`. In rc.3 `.returning()` has **only the
103
- no-argument overload**, so it yields the full row set every time.
104
-
105
- ```ts
106
- // RIGHT: insert, read back the created row
107
- const [row] = await db
108
- .insert(posts)
109
- .values({ title, body, slug, authorId: me.id })
110
- .returning();
111
-
112
- // RIGHT: update by id, read back the updated row
113
- const [row] = await db
114
- .update(posts)
115
- .set({ title, body })
116
- .where(eq(posts.id, id))
117
- .returning();
118
-
119
- // RIGHT: delete by id (no read-back needed)
120
- await db.delete(posts).where(eq(posts.id, id));
121
- ```
122
-
123
- ### Pitfall: `.returning({ ... })` with a field argument
124
-
125
- The older field-selecting `.returning({ id: posts.id })` overload does not exist
126
- in rc.3.
127
-
128
- ```ts
129
- // WRONG (rc.3): TS2554 "Expected 0 arguments, but got 1"
130
- const [row] = await db
131
- .insert(posts)
132
- .values({ title, body, slug, authorId: me.id })
133
- .returning({ id: posts.id, slug: posts.slug });
134
- ```
135
-
136
- Call `.returning()` bare and destructure or map the fields you need in JS.
137
-
138
- ```ts
139
- // RIGHT: full row back, then pick fields
140
- const [row] = await db.insert(posts).values({ ... }).returning();
141
- const created = { id: row.id, slug: row.slug };
142
- ```
143
-
144
- ### `.returning()` yields columns only, never relations
145
-
146
- A `.returning()` row is the table's own columns. It does NOT carry `with`
147
- relations the way a `db.query.*` read does. When the caller expects a joined
148
- shape (an author name alongside the post), insert then re-read, or splice the
149
- already-known related value in by hand.
150
-
151
- ```ts
152
- // RIGHT: insert returns columns; add the known author locally
153
- const [row] = await db.insert(posts).values({ title, body, slug, authorId: me.id }).returning();
154
- return { success: true, data: { ...formatPost(row), authorName: me.name } };
155
- ```
156
-
157
- ---
158
-
159
- ## Column helpers live in `db/columns.server.ts` (the one dialect seam)
160
-
161
- The schema is written against helpers (`table`, `pk`, `uuidPk`, `uuid`, `bool`,
162
- `timestamp`, `createdAt`, `updatedAt`, `index`) rather than raw drizzle
163
- builders, so the same `db/schema.server.ts` runs on SQLite and Postgres. Only
164
- `db/columns.server.ts` differs per dialect. Two of those helpers exist because
165
- rc.3 has not yet exposed a stable no-arg overload.
166
-
167
- - **`table`** wraps `sqliteTableCreator((name) => name, 'snake_case')`. In rc.3
168
- the snake_case casing lives on the table factory, so column keys map to
169
- snake_case SQL names automatically. Do not restate a `snake_case` casing
170
- option on `drizzle()`.
171
- - **`index(...cols)`** wraps rc.3's `index(name)`, which still requires a name
172
- argument the runtime auto-fills. The helper synthesizes drizzle-kit's own
173
- collision-free name, so schema authors call `index(t.createdAt)` with no name.
174
- Replace it with a bare `index()` once 1.0 stable ships the no-arg overload.
175
-
176
- When you add a table, use the helpers (`pk()` for an integer autoincrement id,
177
- `uuidPk()` for an app-generated uuid string id, `createdAt()` for a
178
- default-to-now timestamp) rather than reaching for raw `integer(...).primaryKey(...)`
179
- call sites, so the Postgres swap stays a one-file change.
180
-
181
- ---
182
-
183
- ## Where each query shape belongs
184
-
185
- | Task | API | File |
186
- |---|---|---|
187
- | Row by unique column | `db.query.t.findFirst({ where })` | `queries/*.server.ts` |
188
- | Ordered / filtered list | `db.query.t.findMany({ orderBy, where, with })` | `queries/*.server.ts` |
189
- | Manual join / aggregate | `db.select().from(t)` (no projection arg) | `queries/*.server.ts` |
190
- | Create | `db.insert(t).values(...).returning()` | `actions/*.server.ts` |
191
- | Update | `db.update(t).set(...).where(eq(...)).returning()` | `actions/*.server.ts` |
192
- | Delete | `db.delete(t).where(eq(...))` | `actions/*.server.ts` |
193
-
194
- Cross-references. Derived row TYPES and the `ActionResult<T>` envelope belong in
195
- a browser-safe `modules/<feature>/types.ts` (see `agent-docs/types-and-mutations.md`).
196
- Full page / action / query recipes live in `agent-docs/recipes.md`. The
197
- server-only boundary that keeps `db` off the client is in the root `AGENTS.md`
198
- (invariant 1).