[{"data":1,"prerenderedAt":596},["ShallowReactive",2],{"fougere:post.list:{}":3,"tutorial-steps":6,"tutorial:\u002Ftutorial\u002F7-an-app":43},{"items":4,"hasMore":5},[],false,[7,11,15,19,23,27,31,35,39],{"path":8,"step":9,"name":10},"\u002Ftutorial\u002F0-start",0,"Start",{"path":12,"step":13,"name":14},"\u002Ftutorial\u002F1-an-entity",1,"An entity",{"path":16,"step":17,"name":18},"\u002Ftutorial\u002F2-a-source",2,"A source",{"path":20,"step":21,"name":22},"\u002Ftutorial\u002F3-a-rule",3,"A rule",{"path":24,"step":25,"name":26},"\u002Ftutorial\u002F4-a-port",4,"A port",{"path":28,"step":29,"name":30},"\u002Ftutorial\u002F5-a-link",5,"A link",{"path":32,"step":33,"name":34},"\u002Ftutorial\u002F6-a-fact",6,"A fact",{"path":36,"step":37,"name":38},"\u002Ftutorial\u002F7-an-app",7,"An app",{"path":40,"step":41,"name":42},"\u002Ftutorial\u002F8-split",8,"Split",{"id":44,"title":45,"aside":46,"body":47,"code":53,"concept":534,"description":535,"extension":536,"fig":537,"files":538,"grows":552,"heading":567,"install":568,"lede":535,"links":573,"meta":583,"name":38,"navigation":476,"path":36,"seo":584,"slug":585,"stem":586,"step":37,"tryIt":587,"zero":594,"__hash__":595},"tutorial\u002Ftutorial\u002F7-an-app.md","A page, and nothing declared twice","Publish twice from two tabs: the second click shows Already published, the CONFLICT the handler throws.",{"type":48,"value":49,"toc":50},"minimark",[],{"title":51,"searchDepth":17,"depth":17,"links":52},"",[],[54,77,88,143,175,209,229,240,322,351,361,383,461],{"path":55,"state":51,"lines":56},"fronds\u002Fblog\u002Fentities\u002FPost.ts",[57,60,62,65,68,71,74],{"text":58,"added":5,"keys":59},"import { entity, oneOf, primary, readOnly, text } from '@fougere\u002Fschema';",[],{"text":51,"added":5,"keys":61},[],{"text":63,"added":5,"keys":64},"export default class Post extends entity({",[],{"text":66,"added":5,"keys":67},"  id: primary(),",[],{"text":69,"added":5,"keys":70},"  title: text({ min: 1, max: 160 }),",[],{"text":72,"added":5,"keys":73},"  status: readOnly(oneOf('draft', 'published', { default: 'draft' })),",[],{"text":75,"added":5,"keys":76},"}) {}",[],{"path":78,"state":51,"lines":79},"fronds\u002Fblog\u002Fentities\u002FPostPublished.ts",[80,83,85],{"text":81,"added":5,"keys":82},"import Post from '.\u002FPost.js';",[],{"text":51,"added":5,"keys":84},[],{"text":86,"added":5,"keys":87},"export default class PostPublished extends Post.pick('id', 'title') {}",[],{"path":89,"state":51,"lines":90},"fronds\u002Fblog\u002Fhandlers\u002FPostHandler.ts",[91,94,97,100,102,105,108,111,114,116,119,122,125,128,130,133,135,138,140],{"text":92,"added":5,"keys":93},"import { ErrorCode, FougereError, Repository, type Emit } from '@fougere\u002Fcore';",[],{"text":95,"added":5,"keys":96},"import Post from '..\u002Fentities\u002FPost.js';",[],{"text":98,"added":5,"keys":99},"import type PostPublished from '..\u002Fentities\u002FPostPublished.js';",[],{"text":51,"added":5,"keys":101},[],{"text":103,"added":5,"keys":104},"export default class PostHandler extends Repository\u003CPost> {",[],{"text":106,"added":5,"keys":107},"  constructor(private published: Emit\u003CPostPublished>) {",[],{"text":109,"added":5,"keys":110},"    super();",[],{"text":112,"added":5,"keys":113},"  }",[],{"text":51,"added":5,"keys":115},[],{"text":117,"added":5,"keys":118},"  async publish(id: Post['id']): Promise\u003CPost> {",[],{"text":120,"added":5,"keys":121},"    const post = await this.findById(id);",[],{"text":123,"added":5,"keys":124},"    if (!post) throw new FougereError({ code: ErrorCode.NOT_FOUND, message: 'No such post' });",[],{"text":126,"added":5,"keys":127},"    if (post.status === 'published') throw new FougereError({ code: ErrorCode.CONFLICT, message: 'Already published' });",[],{"text":51,"added":5,"keys":129},[],{"text":131,"added":5,"keys":132},"    await this.published({ id: post.id, title: post.title });",[],{"text":51,"added":5,"keys":134},[],{"text":136,"added":5,"keys":137},"    return this.update(id, { status: 'published' });",[],{"text":112,"added":5,"keys":139},[],{"text":141,"added":5,"keys":142},"}",[],{"path":144,"state":51,"lines":145},"fronds\u002Fmail\u002Fhandlers\u002FMailHandler.ts",[146,149,152,155,157,160,163,165,168,171,173],{"text":147,"added":5,"keys":148},"import type { Fact } from '@fougere\u002Fcore';",[],{"text":150,"added":5,"keys":151},"import type PostPublished from '@fronds\u002Fblog\u002Fentities\u002FPostPublished.js';",[],{"text":153,"added":5,"keys":154},"import Mailer from '..\u002Fservices\u002FMailer.js';",[],{"text":51,"added":5,"keys":156},[],{"text":158,"added":5,"keys":159},"export default class MailHandler {",[],{"text":161,"added":5,"keys":162},"  constructor(private mailer: Mailer) {}",[],{"text":51,"added":5,"keys":164},[],{"text":166,"added":5,"keys":167},"  async onPublished(post: Fact\u003CPostPublished>): Promise\u003Cvoid> {",[],{"text":169,"added":5,"keys":170},"    await this.mailer.send(`Published: ${post.title}`);",[],{"text":112,"added":5,"keys":172},[],{"text":141,"added":5,"keys":174},[],{"path":176,"state":51,"lines":177},"fronds\u002Fmail\u002Fservices\u002FAuditMailer.ts",[178,181,184,186,189,192,194,197,200,202,205,207],{"text":179,"added":5,"keys":180},"import type { Logger } from '@fougere\u002Fcore';",[],{"text":182,"added":5,"keys":183},"import type Mailer from '.\u002FMailer.js';",[],{"text":51,"added":5,"keys":185},[],{"text":187,"added":5,"keys":188},"export default class AuditMailer implements Partial\u003CMailer> {",[],{"text":190,"added":5,"keys":191},"  constructor(private inner: Mailer, private log: Logger) {}",[],{"text":51,"added":5,"keys":193},[],{"text":195,"added":5,"keys":196},"  async send(subject: string): Promise\u003Cvoid> {",[],{"text":198,"added":5,"keys":199},"    this.log.info('sending', subject);",[],{"text":51,"added":5,"keys":201},[],{"text":203,"added":5,"keys":204},"    return this.inner.send(subject);",[],{"text":112,"added":5,"keys":206},[],{"text":141,"added":5,"keys":208},[],{"path":210,"state":51,"lines":211},"fronds\u002Fmail\u002Fservices\u002FConsoleMailer.ts",[212,215,217,220,222,225,227],{"text":213,"added":5,"keys":214},"import Mailer from '.\u002FMailer.js';",[],{"text":51,"added":5,"keys":216},[],{"text":218,"added":5,"keys":219},"export default class ConsoleMailer extends Mailer {",[],{"text":195,"added":5,"keys":221},[],{"text":223,"added":5,"keys":224},"    console.log(`mail: ${subject}`);",[],{"text":112,"added":5,"keys":226},[],{"text":141,"added":5,"keys":228},[],{"path":230,"state":51,"lines":231},"fronds\u002Fmail\u002Fservices\u002FMailer.ts",[232,235,238],{"text":233,"added":5,"keys":234},"export default abstract class Mailer {",[],{"text":236,"added":5,"keys":237},"  abstract send(subject: string): Promise\u003Cvoid>;",[],{"text":141,"added":5,"keys":239},[],{"path":241,"state":242,"lines":243},"apps\u002Fnuxt\u002Fapp\u002Fpages\u002Fposts.vue","new",[244,247,250,253,255,259,263,267,270,272,275,278,281,284,287,290,293,296,298,301,304,307,310,313,316,319],{"text":245,"added":5,"keys":246},"\u003Cscript setup lang=\"ts\">",[],{"text":248,"added":5,"keys":249},"import { post } from '@fronds\u002Ffacade';",[],{"text":251,"added":5,"keys":252},"import Post from '@fronds\u002Fblog\u002Fentities\u002FPost.js';",[],{"text":51,"added":5,"keys":254},[],{"text":256,"added":5,"keys":257},"const { items } = await useQuery(post, 'list');",[258],"a",{"text":260,"added":5,"keys":261},"const { fields, values, errors, submit } = useFormFor(Post);",[262],"b",{"text":264,"added":5,"keys":265},"const publish = useCommand(post, 'publish');",[266],"d",{"text":268,"added":5,"keys":269},"\u003C\u002Fscript>",[],{"text":51,"added":5,"keys":271},[],{"text":273,"added":5,"keys":274},"\u003Ctemplate>",[],{"text":276,"added":5,"keys":277},"  \u003Cform novalidate @submit.prevent=\"submit\">",[],{"text":279,"added":5,"keys":280},"    \u003Ctemplate v-for=\"field in fields\" :key=\"field.name\">",[],{"text":282,"added":5,"keys":283},"      \u003Cinput v-model=\"values[field.name]\" v-bind=\"field.attrs\" :placeholder=\"field.label\" \u002F>",[],{"text":285,"added":5,"keys":286},"      \u003Csmall v-if=\"errors[field.name]\">{{ errors[field.name] }}\u003C\u002Fsmall>",[],{"text":288,"added":5,"keys":289},"    \u003C\u002Ftemplate>",[],{"text":291,"added":5,"keys":292},"    \u003Cbutton>Write a draft\u003C\u002Fbutton>",[],{"text":294,"added":5,"keys":295},"  \u003C\u002Fform>",[],{"text":51,"added":5,"keys":297},[],{"text":299,"added":5,"keys":300},"  \u003Cul>",[],{"text":302,"added":5,"keys":303},"    \u003Cli v-for=\"row in items\" :key=\"row.id\">",[],{"text":305,"added":5,"keys":306},"      {{ row.title }} — {{ row.status }}",[],{"text":308,"added":5,"keys":309},"      \u003Cbutton v-if=\"row.status === 'draft'\" @click=\"publish.execute({ params: { id: row.id } })\">Publish\u003C\u002Fbutton>",[],{"text":311,"added":5,"keys":312},"    \u003C\u002Fli>",[],{"text":314,"added":5,"keys":315},"  \u003C\u002Ful>",[],{"text":317,"added":5,"keys":318},"  \u003Cp v-if=\"publish.error.value\">{{ publish.error.value.message }}\u003C\u002Fp>",[],{"text":320,"added":5,"keys":321},"\u003C\u002Ftemplate>",[],{"path":323,"state":51,"lines":324},"fougere.config.ts",[325,328,331,334,336,339,342,345,348],{"text":326,"added":5,"keys":327},"import '@fougere\u002Fadapter-rest';",[],{"text":329,"added":5,"keys":330},"import '@fougere\u002Fadapter-sql';",[],{"text":332,"added":5,"keys":333},"import { defineFougere } from '@fougere\u002Fcore';",[],{"text":51,"added":5,"keys":335},[],{"text":337,"added":5,"keys":338},"export default defineFougere({",[],{"text":340,"added":5,"keys":341},"  protocols: { rest: {} },",[],{"text":343,"added":5,"keys":344},"  sources: { main: { type: 'sqlite', path: '.data\u002Fblog.db' } },",[],{"text":346,"added":5,"keys":347},"  fronds: { '@fougere\u002Flog': {} },",[],{"text":349,"added":5,"keys":350},"});",[],{"path":352,"state":242,"lines":353},"apps\u002Fnuxt\u002Fapp\u002Fapp.vue",[354,356,359],{"text":273,"added":5,"keys":355},[],{"text":357,"added":5,"keys":358},"  \u003CNuxtPage \u002F>",[],{"text":320,"added":5,"keys":360},[],{"path":362,"state":242,"lines":363},"apps\u002Fnuxt\u002Fnuxt.config.ts",[364,367,370,373,376,380],{"text":365,"added":5,"keys":366},"\u002F\u002F https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fapi\u002Fconfiguration\u002Fnuxt-config",[],{"text":368,"added":5,"keys":369},"export default defineNuxtConfig({",[],{"text":371,"added":5,"keys":372},"  compatibilityDate: '2025-07-15',",[],{"text":374,"added":5,"keys":375},"  devtools: { enabled: true },",[],{"text":377,"added":5,"keys":378},"  modules: ['@fougere\u002Fnuxt'],",[379],"c",{"text":381,"added":5,"keys":382},"})",[],{"path":384,"state":242,"lines":385},"apps\u002Fnuxt\u002Fpackage.json",[386,389,392,395,398,401,404,407,410,413,416,419,422,425,428,431,434,437,440,443,446,449,451,454,457,459],{"text":387,"added":5,"keys":388},"{",[],{"text":390,"added":5,"keys":391},"  \"name\": \"nuxt\",",[],{"text":393,"added":5,"keys":394},"  \"type\": \"module\",",[],{"text":396,"added":5,"keys":397},"  \"private\": true,",[],{"text":399,"added":5,"keys":400},"  \"scripts\": {",[],{"text":402,"added":5,"keys":403},"    \"build\": \"nuxt build\",",[],{"text":405,"added":5,"keys":406},"    \"dev\": \"nuxt dev\",",[],{"text":408,"added":5,"keys":409},"    \"generate\": \"nuxt generate\",",[],{"text":411,"added":5,"keys":412},"    \"preview\": \"nuxt preview\",",[],{"text":414,"added":5,"keys":415},"    \"postinstall\": \"nuxt prepare\"",[],{"text":417,"added":5,"keys":418},"  },",[],{"text":420,"added":5,"keys":421},"  \"dependencies\": {",[],{"text":423,"added":5,"keys":424},"    \"@fougere\u002Fcontainer\": \"latest\",",[],{"text":426,"added":5,"keys":427},"    \"@fougere\u002Fcore\": \"latest\",",[],{"text":429,"added":5,"keys":430},"    \"@fougere\u002Fdefaults\": \"latest\",",[],{"text":432,"added":5,"keys":433},"    \"@fougere\u002Flog\": \"latest\",",[],{"text":435,"added":5,"keys":436},"    \"@fougere\u002Fnuxt\": \"latest\",",[],{"text":438,"added":5,"keys":439},"    \"@fougere\u002Fschema\": \"latest\",",[],{"text":441,"added":5,"keys":442},"    \"nuxt\": \"^4.5.2\",",[],{"text":444,"added":5,"keys":445},"    \"vue\": \"^3.5.43\",",[],{"text":447,"added":5,"keys":448},"    \"vue-router\": \"^5.3.1\"",[],{"text":417,"added":5,"keys":450},[],{"text":452,"added":5,"keys":453},"  \"devDependencies\": {",[],{"text":455,"added":5,"keys":456},"    \"@fougere\u002Fcompiler\": \"latest\"",[],{"text":112,"added":5,"keys":458},[],{"text":141,"added":5,"keys":460},[],{"path":462,"state":463,"lines":464},"package.json","changed",[465,467,470,472,474,478,481,484,486,489,492,494,496,499,502,504,506,508,511,513,515,518,521,524,527,530,532],{"text":387,"added":5,"keys":466},[],{"text":468,"added":5,"keys":469},"  \"name\": \"my-blog\",",[],{"text":396,"added":5,"keys":471},[],{"text":393,"added":5,"keys":473},[],{"text":475,"added":476,"keys":477},"  \"workspaces\": [",true,[],{"text":479,"added":476,"keys":480},"    \"apps\u002F*\"",[],{"text":482,"added":476,"keys":483},"  ],",[],{"text":399,"added":5,"keys":485},[],{"text":487,"added":5,"keys":488},"    \"typecheck\": \"tsc\",",[],{"text":490,"added":5,"keys":491},"    \"test\": \"vitest run --config tests\u002Fvitest.config.ts\"",[],{"text":417,"added":5,"keys":493},[],{"text":420,"added":5,"keys":495},[],{"text":497,"added":5,"keys":498},"    \"@fougere\u002Fadapter-rest\": \"latest\",",[],{"text":500,"added":5,"keys":501},"    \"@fougere\u002Fadapter-sql\": \"latest\",",[],{"text":426,"added":5,"keys":503},[],{"text":432,"added":5,"keys":505},[],{"text":438,"added":5,"keys":507},[],{"text":509,"added":5,"keys":510},"    \"better-sqlite3\": \"^13.0.3\"",[],{"text":417,"added":5,"keys":512},[],{"text":452,"added":5,"keys":514},[],{"text":516,"added":5,"keys":517},"    \"@fougere\u002Fcli\": \"latest\",",[],{"text":519,"added":5,"keys":520},"    \"@fougere\u002Ftesting\": \"latest\",",[],{"text":522,"added":5,"keys":523},"    \"@types\u002Fnode\": \"^22.19.15\",",[],{"text":525,"added":5,"keys":526},"    \"typescript\": \"^7.0.2\",",[],{"text":528,"added":5,"keys":529},"    \"vitest\": \"^4.1.0\"",[],{"text":112,"added":5,"keys":531},[],{"text":141,"added":5,"keys":533},[],"facade","We add a Nuxt app in apps\u002Fnuxt with the @fougere\u002Fnuxt module and write a page. The page calls the blog by its name, and the fronds run inside Nuxt.","md","The page names post and an operation. Its rows, its form and its refusals are read from what you already wrote.",[539,540,541,542,543,544,545,546,547,548,549,550,551],{"path":55,"state":51},{"path":78,"state":51},{"path":89,"state":51},{"path":144,"state":51},{"path":176,"state":51},{"path":210,"state":51},{"path":230,"state":51},{"path":241,"state":242},{"path":323,"state":51},{"path":352,"state":242},{"path":362,"state":242},{"path":384,"state":242},{"path":462,"state":463},[553,557,560,563],{"key":258,"side":554,"label":555,"detail":556},"in","useQuery(post, 'list')","the posts, typed as Post — no row shape written",{"key":262,"side":554,"label":558,"detail":559},"useFormFor(Post)","the fields, their rules, an error under each — status is left out, it is readOnly",{"key":379,"side":554,"label":561,"detail":562},"the fronds inside Nuxt","the module boots them and serves \u002Fapi and \u002F_fougere\u002Fcall on :3000",{"key":266,"side":564,"label":565,"detail":566},"refuses","publish.error","typed NOT_FOUND | CONFLICT — what publish can refuse, read from its code","A page, *and nothing declared twice*",{"npm":569,"pnpm":570,"bun":571,"yarn":572},"npm create nuxt@latest apps\u002Fnuxt && cd apps\u002Fnuxt && npm install @fougere\u002Fnuxt","pnpm create nuxt@latest apps\u002Fnuxt && cd apps\u002Fnuxt && pnpm add @fougere\u002Fnuxt","bun create nuxt@latest apps\u002Fnuxt && cd apps\u002Fnuxt && bun add @fougere\u002Fnuxt","yarn create nuxt@latest apps\u002Fnuxt && cd apps\u002Fnuxt && yarn add @fougere\u002Fnuxt",[574,577,580],{"title":575,"path":576},"Pages","\u002Fdocs\u002Fserve\u002Fpages",{"title":578,"path":579},"A form","\u002Fdocs\u002Fserve\u002Fforms",{"title":581,"path":582},"Pick a host","\u002Fdocs\u002Fadopt\u002Fhosts",{},{"title":45,"description":535},"7-an-app","tutorial\u002F7-an-app",{"command":588,"said":589,"output":593},"cd apps\u002Fnuxt && npm run dev   # then open localhost:3000\u002Fposts",{"npm":588,"pnpm":590,"bun":591,"yarn":592},"cd apps\u002Fnuxt && pnpm dev   # then open localhost:3000\u002Fposts","cd apps\u002Fnuxt && bun run dev   # then open localhost:3000\u002Fposts","cd apps\u002Fnuxt && yarn dev   # then open localhost:3000\u002Fposts","a form with one field, title — and the posts, each with Publish","fronds\u002F","LEJvLiJzp1NZuAOavRHZntO_bJc7O97m_ixLOO9YV8M",1794396615759]