navori 0.2.11 → 0.2.16

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/README.md CHANGED
@@ -19,6 +19,9 @@ npx navori init
19
19
  cd ~/tu-repo
20
20
  navori init --recommended
21
21
 
22
+ # Instalación máxima: todos los plugins + pre-commit hook + scan-monorepo + project block estricto
23
+ navori init --full
24
+
22
25
  # O wizard interactivo con detección de stack
23
26
  navori init
24
27
  ```
@@ -40,7 +43,7 @@ Y genera:
40
43
 
41
44
  | Comando | Qué hace |
42
45
  |---|---|
43
- | `init` | Bootstrap del repo con detección automática + wizard (o `--recommended` sin preguntas) |
46
+ | `init` | Bootstrap del repo con detección automática + wizard (o `--recommended` sin preguntas, o `--full` para la instalación máxima) |
44
47
  | `add <plugin>` | Activa un plugin y opcionalmente instala la tool externa |
45
48
  | `configure <section>` | Ajusta una sección del config sin re-correr el wizard |
46
49
  | `update` | Re-detecta el repo, refresca config y corre sync en un paso |
@@ -67,10 +70,12 @@ Un preset aporta skills y reglas específicas del stack además del core. El `in
67
70
  | `vite-react-ts` | Vite + React + TS (SPA, agnóstico de UI-lib) |
68
71
  | `vite-react-ts-mantine` | Vite + React + TS + Mantine (SPA) |
69
72
  | `nextjs` | Next.js (App Router) |
73
+ | `react-native-expo` | React Native + Expo (app móvil) |
70
74
  | `astro` | Astro (static / SSR) |
71
75
  | `nestjs` | NestJS (backend) |
72
76
  | `express` | Express (backend, agnóstico de DB) |
73
77
  | `express-mongoose` | Express + Mongoose (backend) |
78
+ | `bun-keystone` | Keystone 6 + Prisma (backend, Bun) |
74
79
  | `background-worker` | Worker de fondo (jobs + colas: agenda / bullmq / amqplib) |
75
80
  | `medusa` | Medusa.js v2 (backend) |
76
81
 
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: bullmq
3
+ description: Jobs y colas con BullMQ sobre Redis — Queue/Worker/QueueEvents, jobs idempotentes, retries con backoff, concurrency y graceful shutdown. Aplica al crear/tocar un job, un worker o al encolar trabajo async.
4
+ type: reference
5
+ ---
6
+
7
+ # BullMQ — jobs & queues
8
+
9
+ BullMQ mueve trabajo pesado o diferido fuera del request: un **productor** encola (`Queue.add`) y un **worker** (proceso aparte) lo procesa. La conexión es Redis (`ioredis`). El productor y el worker viven en procesos distintos y comparten solo el nombre de la cola.
10
+
11
+ ## Cuándo usar este skill
12
+
13
+ Al crear una cola o un job nuevo, tocar el worker, encolar trabajo desde un handler/hook, o depurar jobs que se cuelgan, se reintentan en loop o se pierden.
14
+
15
+ ## El patrón
16
+
17
+ ```ts
18
+ // Productor (en un request/hook): encola y responde rápido, NO esperes el resultado.
19
+ await queue.add("send-welcome", { userId }, {
20
+ attempts: 3,
21
+ backoff: { type: "exponential", delay: 1000 },
22
+ removeOnComplete: 1000, // no dejes que Redis crezca sin límite
23
+ removeOnFail: 5000,
24
+ });
25
+
26
+ // Worker (proceso aparte): una responsabilidad por worker/cola.
27
+ const worker = new Worker("emails", async (job) => {
28
+ // idempotente: correr dos veces el mismo job no debe duplicar efectos
29
+ return sendEmail(job.data);
30
+ }, { connection, concurrency: 5 });
31
+ ```
32
+
33
+ ## Reglas duras
34
+
35
+ 1. **Jobs idempotentes.** Un job puede reintentarse o entregarse dos veces. Usa un id determinista (`jobId`) o un guard de "ya procesado" para efectos no repetibles (cobros, emails, mutaciones críticas).
36
+ 2. **`attempts` + `backoff` siempre.** Un job sin reintentos muere al primer error transitorio; uno sin backoff martillea el recurso que falla. Exponencial por default.
37
+ 3. **El productor NO espera el resultado.** Encola y responde; el valor del job se consume por eventos (`QueueEvents`) o releyendo estado, no bloqueando el request.
38
+ 4. **`removeOnComplete`/`removeOnFail`.** Sin límites, Redis se llena de jobs viejos. Acota siempre.
39
+ 5. **Graceful shutdown.** En `SIGTERM`/`SIGINT`, `await worker.close()` antes de salir para no matar un job a medias. Un worker que no cierra limpio deja jobs en `active` colgados.
40
+ 6. **Errores que deben reintentar → lanza; errores permanentes → no.** Un input inválido no se arregla reintentando: valida antes de encolar o marca el job como fallido sin reintento (`attempts: 1` o un error no-recuperable).
41
+ 7. **Una responsabilidad por worker.** No metas varios tipos de trabajo no relacionado en un solo `Worker` con `if job.name`; sepáralos por cola.
42
+
43
+ ## Gotchas que muerden
44
+
45
+ - **La `connection` de ioredis para BullMQ necesita `maxRetriesPerRequest: null`** — si no, BullMQ lanza al reconectar.
46
+ - **`concurrency` alto no es gratis**: cada job concurrente abre conexiones/CPU. Súbelo con medida, no por default.
47
+ - **Un job "perdido"** casi siempre es: el worker no está corriendo, apunta a otra cola/Redis, o crasheó sin `removeOnFail` y quedó en `failed`. Revisa el estado del job antes de asumir un bug de lógica.
48
+ - **Delayed/repeatable jobs** viven en Redis: cambiar el patrón de un repeatable no borra el viejo — límpialo explícitamente.
49
+
50
+ ## Antes de declarar listo
51
+
52
+ - El job es idempotente (o tiene guard de duplicados) y define `attempts` + `backoff`.
53
+ - El worker cierra en `SIGTERM`/`SIGINT` (`worker.close()`).
54
+ - `removeOnComplete`/`removeOnFail` acotados; la `connection` usa `maxRetriesPerRequest: null`.
55
+ - El productor no bloquea el request esperando el job.
56
+ - `{{qualityGate.fast}}` en verde.
@@ -0,0 +1,13 @@
1
+ ## Stack — Keystone 6 (Bun + Prisma)
2
+
3
+ Backend GraphQL sobre **Keystone 6**, runtime **Bun**, persistencia **Prisma + PostgreSQL**. Los datos se modelan como *lists* (`list({ access, hooks, fields })`) y Keystone deriva de ahí el schema Prisma y la API GraphQL: `schema.prisma` y `schema.graphql` son **autogenerados**, nunca se editan a mano (ver `prisma-keystone`).
4
+
5
+ Tres contratos gobiernan todo el código de datos:
6
+
7
+ - **Access control en 3 capas** — cada list declara `operation`, `filter` y `field`; `allowAll` está prohibido. Una sesión nula recibe un filtro restrictivo, nunca abierto. Ver `keystone-access`.
8
+ - **Hooks con contrato estricto** — `resolveInput` retorna datos, `validateInput` lanza `Error` (nunca retorna un valor), `afterOperation` chequea `operation` antes de actuar. Ver `keystone-models`.
9
+ - **`context.sudo()` en hooks y services** — nunca `context.db` (aplicaría el access de la sesión actual) ni Prisma directo; `context.prisma` queda solo para scripts de seed/migración.
10
+
11
+ Toda dependencia externa (SMS, pagos, APIs de terceros) va detrás de una interfaz en `[servicio].adapter.ts`: los services reciben la interfaz, no la implementación, para poder mockearla en tests.
12
+
13
+ **Eficiencia de contexto** — los artefactos generados (`types/graphql.ts` puede rondar decenas de miles de tokens, `schema.graphql`, `migrations/`, el lockfile) **no se leen completos**: infiere los tipos desde la *list* en `models/` o desde `schema.prisma`, y busca con `grep`/`Grep` en vez de abrir el archivo entero.
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: keystone-access
3
+ description: Access control de Keystone 6 en 3 capas (operation / filter / field). allowAll prohibido; sesión nula → filtro restrictivo. Aplica al definir o cambiar el access de cualquier list.
4
+ type: reference
5
+ ---
6
+
7
+ # Keystone Access Control — 3 capas
8
+
9
+ ## Cuándo usar este skill
10
+
11
+ Siempre que definas o modifiques el `access` de una list. Una línea mal puesta aquí es una fuga de datos o un bloqueo total — es el código más sensible del backend. Léelo completo antes de tocar `access`.
12
+
13
+ ## Las 3 capas
14
+
15
+ ```ts
16
+ access: {
17
+ operation: { query, create, update, delete }, // ¿puede el usuario ejecutar la operación?
18
+ filter: { query, update, delete }, // ¿sobre QUÉ registros? (devuelve un where)
19
+ field: { fieldName: { read, create, update } }, // ¿puede leer/escribir ESTE campo?
20
+ }
21
+ ```
22
+
23
+ 1. **`operation`** — gate booleano por operación. Devuelve `true`/`false` según la sesión. Es el "¿tiene permiso de intentarlo?".
24
+ 2. **`filter`** — devuelve un `where` de Prisma que acota el conjunto de registros visibles/afectables. Es el "¿sobre cuáles?". Ej.: un usuario solo ve/edita sus propios registros → `{ author: { id: { equals: session.itemId } } }`.
25
+ 3. **`field`** — control fino por campo (ocultar un campo sensible en lectura, impedir escribir un campo calculado).
26
+
27
+ Las tres se combinan: `operation` decide si la request entra, `filter` acota el set, `field` recorta columnas.
28
+
29
+ ## Reglas duras
30
+
31
+ 1. **`allowAll` está prohibido.** Nunca `access: allowAll`. Todo list declara reglas explícitas por operación. Si algo "es público", exprésalo con una función que retorna `true` acotada, no con `allowAll`.
32
+ 2. **Sesión nula → restrictivo, no abierto.** Cuando no hay sesión, el default es negar (o un `filter` que no matchee nada), nunca abrir. Empieza cerrando y abre lo justo.
33
+ 3. **`filter` devuelve un where, no un booleano.** Si necesitas negar todo en una capa `filter`, devuelve un where imposible (`{ id: { equals: null } }`), no `false`.
34
+ 4. **La lógica de access va en `access/`, no inline.** Extrae funciones reutilizables (`isSignedIn`, `isOwner`, `isAdmin`) a archivos de `access/` y compón; no dupliques la misma condición inline en varias lists.
35
+ 5. **Access ≠ validación de negocio.** Access decide quién ve/toca qué; las reglas de negocio (un valor válido, un estado permitido) van en `validateInput` (ver `keystone-models`).
36
+
37
+ ## Tabla rápida
38
+
39
+ | Quiero | Capa | Forma |
40
+ |---|---|---|
41
+ | Bloquear crear a no-admins | `operation.create` | `({ session }) => isAdmin(session)` |
42
+ | Que cada quien vea lo suyo | `filter.query` | `({ session }) => ({ owner: { id: { equals: session?.itemId } } })` |
43
+ | Ocultar un campo sensible | `field.<campo>.read` | `({ session }) => isAdmin(session)` |
44
+ | Impedir editar un campo calculado | `field.<campo>.update` | `() => false` |
45
+
46
+ ## Antes de declarar el cambio "listo"
47
+
48
+ - `{{qualityGate.fast}}` en verde.
49
+ - `grep -rn "allowAll" access/ models/` → 0 resultados.
50
+ - Toda list tocada declara las 3 capas donde apliquen; ninguna operación quedó implícitamente abierta.
51
+ - Sesión nula probada: la list niega o filtra, nunca expone todo.
52
+ - Las condiciones nuevas se extrajeron a `access/` si se repiten en más de una list.
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: keystone-models
3
+ description: Convenciones para lists de Keystone 6 — estructura list({ access, hooks, fields }), contrato de hooks (resolveInput/validateInput/afterOperation) y uso de context.sudo(). Aplica al crear o modificar un modelo.
4
+ type: reference
5
+ ---
6
+
7
+ # Keystone Models — convenciones del proyecto
8
+
9
+ ## Cuándo usar este skill
10
+
11
+ Antes de crear una list nueva, agregar/cambiar un field, o tocar un hook de un modelo. Los hooks y el access de una list son el punto donde vive la lógica de negocio y la seguridad de los datos; saltarse el contrato rompe la integridad o abre huecos de acceso.
12
+
13
+ ## Estructura de una list
14
+
15
+ ```ts
16
+ export const Report = list({
17
+ access: { /* ver skill keystone-access */ },
18
+ hooks: { resolveInput, validateInput, afterOperation },
19
+ fields: {
20
+ title: text({ validation: { isRequired: true } }),
21
+ author: relationship({ ref: "User.reports", many: false }),
22
+ // ...
23
+ },
24
+ });
25
+ ```
26
+
27
+ Un modelo se compone de tres bloques: `access` (quién puede qué — skill aparte), `hooks` (lógica de dominio en el ciclo de vida) y `fields` (forma de los datos). Manténlos en ese orden.
28
+
29
+ ## Contrato de hooks (reglas duras)
30
+
31
+ 1. **`resolveInput` transforma y retorna** — devuelve el objeto de datos resuelto: `return { ...resolvedData, slug };`. Es el único hook que muta lo que se va a persistir. Nunca lanzas desde aquí para validar (eso es `validateInput`).
32
+ 2. **`validateInput` valida y lanza** — chequea invariantes de negocio y, si algo está mal, `addValidationError(msg)` o `throw new Error(msg)`. **Nunca retorna un valor**; su único efecto es dejar pasar o abortar la operación.
33
+ 3. **`afterOperation` reacciona** — corre después de persistir (side-effects: encolar un job, recalcular un agregado, emitir un evento). **Siempre** chequea `operation` antes de actuar: `if (operation === "create" || operation === "update") { ... }`. En `delete` los datos ya no existen — usa `originalItem`.
34
+
35
+ ## context.sudo() cheatsheet
36
+
37
+ ```ts
38
+ context.sudo().db.Model; // hooks + services: bypass del access, para lógica interna confiable
39
+ context.db.Model; // NUNCA en hooks/services — re-aplica el access de la sesión y puede filtrar/negar de más
40
+ context.prisma; // SOLO en scripts de seed/migración, nunca en runtime de la app
41
+ ```
42
+
43
+ Dentro de un hook o service **siempre** usa `context.sudo()`. Usar `context.db` en un hook es un bug latente: la operación puede fallar o devolver datos parciales según quién esté logueado.
44
+
45
+ ## Tabla rápida
46
+
47
+ | Necesito | Dónde / Cómo |
48
+ |---|---|
49
+ | Derivar un campo antes de guardar | `resolveInput` → `return { ...resolvedData, campo }` |
50
+ | Rechazar una operación inválida | `validateInput` → `throw new Error(...)` / `addValidationError(...)` |
51
+ | Efecto secundario tras guardar | `afterOperation` con guard `operation === 'create'\|'update'` |
52
+ | Leer/escribir otro modelo desde un hook | `context.sudo().db.OtroModelo` |
53
+ | Relación entre modelos | `relationship({ ref: "Otro.campoInverso" })` |
54
+
55
+ ## Antes de declarar el cambio "listo"
56
+
57
+ - `{{qualityGate.fast}}` en verde.
58
+ - Ningún hook retorna desde `validateInput` ni lanza desde `resolveInput`.
59
+ - Ningún `afterOperation` actúa sin chequear `operation`.
60
+ - Ningún `context.db` ni `context.prisma` dentro de hooks/services (usa `context.sudo()`).
61
+ - Si agregaste un field a un modelo existente: corre la migración (ver `prisma-keystone`), no edites `schema.prisma` a mano.
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: keystone-rest
3
+ description: Endpoints REST/Express sobre Keystone 6 — el controller valida el input con Zod safeParse y delega a un service que usa context.sudo().db. Aplica al crear o tocar una ruta, un controller o un service REST.
4
+ type: reference
5
+ ---
6
+
7
+ # Keystone REST — controller fino, service con la lógica
8
+
9
+ Keystone expone GraphQL, pero un backend suele montar además rutas REST/Express (vía `extendExpressApp` o un router propio) para webhooks, integraciones y endpoints a medida. La regla: el **controller** solo habla HTTP; la **lógica de datos vive en un service** que usa el `context` de Keystone.
10
+
11
+ ## Cuándo usar este skill
12
+
13
+ Al crear o tocar una ruta REST, su controller o el service que hay detrás; o al depurar un endpoint que valida mal, filtra de más o expone internals.
14
+
15
+ ## El patrón
16
+
17
+ ```ts
18
+ // controller — SOLO HTTP: valida el borde y delega. Nunca lógica de negocio aquí.
19
+ export async function createReport(req: Request, res: Response) {
20
+ const parsed = CreateReportSchema.safeParse(req.body); // safeParse, NUNCA parse
21
+ if (!parsed.success) return sendError(res, "Input inválido", 422);
22
+ const report = await reportService.create(parsed.data, req.context);
23
+ return sendSuccess(res, { data: report }, 201);
24
+ }
25
+
26
+ // service — la lógica; recibe context, no (req, res). context.sudo().db, no context.db.
27
+ export const reportService = {
28
+ /** Crea un Report aplicando las reglas de negocio (no el access de la sesión). */
29
+ async create(input: CreateReportInput, context: Context) {
30
+ return context.sudo().db.Report.createOne({ data: input });
31
+ },
32
+ };
33
+ ```
34
+
35
+ ## Reglas duras
36
+
37
+ 1. **`safeParse`, nunca `parse`.** El input HTTP es hostil: valídalo con el schema Zod y responde 4xx ante el fallo; una excepción sin capturar se fuga como 500.
38
+ 2. **Controller fino.** El controller no accede a la DB ni implementa reglas: parsea, delega al service, formatea la respuesta. Toda la lógica testeable vive en el service.
39
+ 3. **El service recibe `context`, no `req`/`res`.** Así se testea sin HTTP y se reusa desde otro controller, un hook o un job. Dentro usa `context.sudo().db` (ver `keystone-access` para el porqué de `sudo`).
40
+ 4. **Errores sin internals.** Nunca devuelvas stacktraces, SQL ni mensajes de librería al cliente; loguea el detalle y responde un mensaje acotado con su código.
41
+ 5. **Respuestas consistentes.** Éxito y error pasan por un formateador único (un `sendSuccess`/`sendError` o equivalente), no por `res.json` suelto en cada controller.
42
+
43
+ ## Antes de declarar el cambio "listo"
44
+
45
+ - Ningún controller llama a `.parse(` sobre el input HTTP (búscalo: debe ser `safeParse`).
46
+ - Ningún controller usa `context.db.` directo — la lógica está en un service con `context.sudo().db`.
47
+ - El service nuevo/tocado tiene unit tests (recibe un `context` mockeado; ver `keystone-testing`).
@@ -0,0 +1,72 @@
1
+ ---
2
+ name: keystone-testing
3
+ description: Testing de Keystone 6 con Vitest — hooks y access con context mockeado, endpoints GraphQL/REST con Supertest, factories. Aplica al escribir o revisar tests de models, access, hooks o API.
4
+ type: reference
5
+ ---
6
+
7
+ # Keystone Testing — Vitest + Supertest
8
+
9
+ Dos niveles: **unit** (hooks/access/services con el `context` de Keystone mockeado, sin DB) e **integration/e2e** (API GraphQL/REST real con Supertest contra una instancia de Keystone y una DB de test). El unit es rápido y cubre la lógica; el integration cubre el contrato.
10
+
11
+ ## Cuándo usar este skill
12
+
13
+ Al escribir tests para un hook, una función de `access/`, un service o un endpoint; al elegir el nivel (unit vs integration); o al depurar un test flaky de mocks.
14
+
15
+ ## Unit — hooks y access con context mockeado
16
+
17
+ Los hooks y las funciones de access son funciones puras sobre `{ session, context, ... }`: se testean sin DB, mockeando el `context`.
18
+
19
+ ```ts
20
+ // El mock de context expone sudo().db.<Model> y query; devuélvelo desde un helper reusable.
21
+ const context = makeMockContext({ session: adminSession });
22
+
23
+ it("validateInput rechaza truthState manual", async () => {
24
+ await expect(
25
+ Report.hooks.validateInput({ resolvedData: { truthState: "TRUE" }, operation: "create", context }),
26
+ ).rejects.toThrow();
27
+ });
28
+
29
+ it("access.filter.query acota a los registros del dueño", () => {
30
+ expect(reportAccess.filter.query({ session: userSession })).toEqual({
31
+ author: { id: { equals: userSession.itemId } },
32
+ });
33
+ });
34
+ ```
35
+
36
+ Testea **cada capa de access por separado** (`operation`/`filter`/`field`) y **la sesión nula** (debe negar/filtrar, nunca abrir).
37
+
38
+ ## Integration — API con Supertest
39
+
40
+ Levanta Keystone contra una DB de test y golpea el endpoint real (valida el contrato completo: access + hooks + resolvers).
41
+
42
+ ```ts
43
+ const res = await request(app)
44
+ .post("/api/graphql")
45
+ .set("Cookie", authCookie)
46
+ .send({ query: `mutation { createReport(data: {...}) { id } }` });
47
+ expect(res.status).toBe(200);
48
+ expect(res.body.errors).toBeUndefined();
49
+ ```
50
+
51
+ La DB de test se levanta/migra/siembra antes y se derriba después (scripts `test:db:*` / `test:e2e`). Requiere Docker.
52
+
53
+ ## Reglas duras
54
+
55
+ 1. **Factories, no fixtures inline.** Centraliza la construcción de datos de test en factories (`test-factories`) y la sesión en helpers (`test-auth`); no repitas objetos `session`/`data` en cada archivo.
56
+ 2. **Un mock de context reusable.** El mock de `context` (con `sudo().db`) vive en un helper compartido, no re-inventado por test.
57
+ 3. **Access probado en las 3 capas + sesión nula.** Es el código más sensible; cada capa y el caso sin sesión tienen su test.
58
+ 4. **Trazabilidad SDD.** En features SDD-scope, cada `R<n>` se cubre con ≥1 test que lo referencia en nombre o comentario `// Covers: R<n>`.
59
+ 5. **No generar tests salvo que se pidan** (si el proyecto así lo define); cuando se piden, van al nivel correcto (unit para lógica, integration para contrato).
60
+
61
+ ## Gotchas de Vitest (v4.x)
62
+
63
+ - **`vi.hoisted()`** para factories que usan variables externas dentro de `vi.mock` (el mock se hoistea sobre las declaraciones).
64
+ - **Mock-constructor con función regular, no arrow** (una arrow no es `new`-able).
65
+ - **`vi.clearAllMocks()` NO limpia implementaciones** (solo `mock.calls`); usa `vi.resetAllMocks()`/`restoreAllMocks()` cuando necesites resetear la implementación.
66
+
67
+ ## Antes de declarar listo
68
+
69
+ - Los hooks/access nuevos o tocados tienen unit tests (incluida la sesión nula).
70
+ - Los datos de test salen de factories; el context sale del mock compartido.
71
+ - Si es SDD-scope, cada `R<n>` es trazable a un test.
72
+ - `{{qualityGate.fast}}` en verde (los tests con Docker corren con el gate completo).
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: prisma-keystone
3
+ description: Prisma bajo Keystone 6 — schema.prisma autogenerado (no editar a mano), migraciones vía keystone prisma migrate, y context.prisma solo en scripts. Aplica al cambiar la forma de datos o correr migraciones.
4
+ type: reference
5
+ ---
6
+
7
+ # Prisma bajo Keystone
8
+
9
+ ## Cuándo usar este skill
10
+
11
+ Al agregar/cambiar un field o una list (cambia la forma de la BD), al correr o revisar migraciones, o al escribir un script de seed/backfill. El error clásico es editar `schema.prisma` o `schema.graphql` a mano — ambos son artefactos generados y tu cambio se pierde en la siguiente generación.
12
+
13
+ ## Regla base: el schema es derivado, no fuente
14
+
15
+ - **`schema.prisma` y `schema.graphql` son autogenerados por Keystone** a partir de las lists. La fuente de verdad son los archivos de `models/`. **Nunca los edites a mano.**
16
+ - Para cambiar la BD: edita la list (field nuevo, cambio de tipo, relación), regenera y migra. Keystone reescribe el schema.
17
+ - No leas `schema.prisma` completo para "entender los tipos" — infiérelos desde la list o busca con `grep`. Es largo y derivado.
18
+
19
+ ## Migraciones (vía Keystone, no Prisma directo)
20
+
21
+ ```bash
22
+ # Desarrollo: genera + aplica una migración a partir del cambio en las lists
23
+ keystone prisma migrate dev --name <descripcion-corta>
24
+
25
+ # Producción / deploy: aplica migraciones ya generadas
26
+ keystone prisma migrate deploy
27
+ ```
28
+
29
+ Usa siempre `keystone prisma ...` (respeta la config de Keystone), no `prisma migrate` suelto. Revisa el SQL generado antes de commitear la migración: una migración destructiva (drop de columna con datos) necesita un plan de datos, no solo el cambio de schema.
30
+
31
+ ## context.prisma — solo en scripts
32
+
33
+ ```ts
34
+ context.sudo().db.Model; // runtime de la app (hooks/services) — ver keystone-models
35
+ context.prisma; // SOLO scripts de seed/migración/backfill — nunca en runtime
36
+ ```
37
+
38
+ `context.prisma` te da el cliente Prisma crudo (sin access ni hooks de Keystone). Es la herramienta correcta para un seed o un backfill masivo, y la herramienta incorrecta dentro de un hook o un resolver — ahí siempre `context.sudo().db`.
39
+
40
+ ## Antes de declarar el cambio "listo"
41
+
42
+ - `{{qualityGate.fast}}` en verde.
43
+ - Ni `schema.prisma` ni `schema.graphql` fueron editados a mano (aparecen solo como salida de la regeneración).
44
+ - Toda migración nueva está commiteada junto al cambio de la list que la origina.
45
+ - Ningún `context.prisma` fuera de `scripts/`.
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "https://navori.dev/schema/navori.preset.v1.json",
3
+ "id": "bun-keystone",
4
+ "displayName": "Keystone 6 backend (Bun + Prisma)",
5
+ "extends": "core",
6
+ "extras": {
7
+ "managed": [
8
+ {
9
+ "id": "stack-bun-keystone",
10
+ "relPath": "presets/bun-keystone/managed/stack.md"
11
+ }
12
+ ],
13
+ "agents": [],
14
+ "skills": [
15
+ {
16
+ "id": "keystone-models",
17
+ "relPath": "presets/bun-keystone/skills/keystone-models.md",
18
+ "destRelPath": ".claude/skills/keystone-models.md"
19
+ },
20
+ {
21
+ "id": "keystone-access",
22
+ "relPath": "presets/bun-keystone/skills/keystone-access.md",
23
+ "destRelPath": ".claude/skills/keystone-access.md"
24
+ },
25
+ {
26
+ "id": "prisma-keystone",
27
+ "relPath": "presets/bun-keystone/skills/prisma-keystone.md",
28
+ "destRelPath": ".claude/skills/prisma-keystone.md"
29
+ },
30
+ {
31
+ "id": "keystone-testing",
32
+ "relPath": "presets/bun-keystone/skills/keystone-testing.md",
33
+ "destRelPath": ".claude/skills/keystone-testing.md"
34
+ },
35
+ {
36
+ "id": "keystone-rest",
37
+ "relPath": "presets/bun-keystone/skills/keystone-rest.md",
38
+ "destRelPath": ".claude/skills/keystone-rest.md"
39
+ }
40
+ ],
41
+ "hooks": []
42
+ },
43
+ "invariants": ["allowAll", "context.sudo()", "validateInput"]
44
+ }