[{"data":1,"prerenderedAt":345},["ShallowReactive",2],{"fougere:post.list:{}":3,"tutorial-steps":6,"tutorial:\u002Ftutorial\u002F5-a-link":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":302,"description":303,"extension":304,"fig":305,"files":306,"grows":314,"heading":326,"install":46,"lede":303,"links":327,"meta":334,"name":30,"navigation":335,"path":28,"seo":336,"slug":337,"stem":338,"step":29,"tryIt":339,"zero":343,"__hash__":344},"tutorial\u002Ftutorial\u002F5-a-link.md","Stand in front of the port",null,{"type":48,"value":49,"toc":50},"minimark",[],{"title":51,"searchDepth":17,"depth":17,"links":52},"",[],[54,77,132,170,190,201,230],{"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\u002Fhandlers\u002FPostHandler.ts",[80,83,86,89,91,94,97,100,103,105,108,111,114,117,119,122,124,127,129],{"text":81,"added":5,"keys":82},"import { ErrorCode, FougereError, Repository } from '@fougere\u002Fcore';",[],{"text":84,"added":5,"keys":85},"import Post from '..\u002Fentities\u002FPost.js';",[],{"text":87,"added":5,"keys":88},"import Mailer from '..\u002Fservices\u002FMailer.js';",[],{"text":51,"added":5,"keys":90},[],{"text":92,"added":5,"keys":93},"export default class PostHandler extends Repository\u003CPost> {",[],{"text":95,"added":5,"keys":96},"  constructor(private mailer: Mailer) {",[],{"text":98,"added":5,"keys":99},"    super();",[],{"text":101,"added":5,"keys":102},"  }",[],{"text":51,"added":5,"keys":104},[],{"text":106,"added":5,"keys":107},"  async publish(id: Post['id']): Promise\u003CPost> {",[],{"text":109,"added":5,"keys":110},"    const post = await this.findById(id);",[],{"text":112,"added":5,"keys":113},"    if (!post) throw new FougereError({ code: ErrorCode.NOT_FOUND, message: 'No such post' });",[],{"text":115,"added":5,"keys":116},"    if (post.status === 'published') throw new FougereError({ code: ErrorCode.CONFLICT, message: 'Already published' });",[],{"text":51,"added":5,"keys":118},[],{"text":120,"added":5,"keys":121},"    await this.mailer.send(`Published: ${post.title}`);",[],{"text":51,"added":5,"keys":123},[],{"text":125,"added":5,"keys":126},"    return this.update(id, { status: 'published' });",[],{"text":101,"added":5,"keys":128},[],{"text":130,"added":5,"keys":131},"}",[],{"path":133,"state":134,"lines":135},"fronds\u002Fblog\u002Fservices\u002FAuditMailer.ts","new",[136,139,142,144,148,152,154,157,161,163,166,168],{"text":137,"added":5,"keys":138},"import type { Logger } from '@fougere\u002Fcore';",[],{"text":140,"added":5,"keys":141},"import type Mailer from '.\u002FMailer.js';",[],{"text":51,"added":5,"keys":143},[],{"text":145,"added":5,"keys":146},"export default class AuditMailer implements Partial\u003CMailer> {",[147],"c",{"text":149,"added":5,"keys":150},"  constructor(private inner: Mailer, private log: Logger) {}",[151],"a",{"text":51,"added":5,"keys":153},[],{"text":155,"added":5,"keys":156},"  async send(subject: string): Promise\u003Cvoid> {",[],{"text":158,"added":5,"keys":159},"    this.log.info('sending', subject);",[160],"b",{"text":51,"added":5,"keys":162},[],{"text":164,"added":5,"keys":165},"    return this.inner.send(subject);",[],{"text":101,"added":5,"keys":167},[],{"text":130,"added":5,"keys":169},[],{"path":171,"state":51,"lines":172},"fronds\u002Fblog\u002Fservices\u002FConsoleMailer.ts",[173,176,178,181,183,186,188],{"text":174,"added":5,"keys":175},"import Mailer from '.\u002FMailer.js';",[],{"text":51,"added":5,"keys":177},[],{"text":179,"added":5,"keys":180},"export default class ConsoleMailer extends Mailer {",[],{"text":155,"added":5,"keys":182},[],{"text":184,"added":5,"keys":185},"    console.log(`mail: ${subject}`);",[],{"text":101,"added":5,"keys":187},[],{"text":130,"added":5,"keys":189},[],{"path":191,"state":51,"lines":192},"fronds\u002Fblog\u002Fservices\u002FMailer.ts",[193,196,199],{"text":194,"added":5,"keys":195},"export default abstract class Mailer {",[],{"text":197,"added":5,"keys":198},"  abstract send(subject: string): Promise\u003Cvoid>;",[],{"text":130,"added":5,"keys":200},[],{"path":202,"state":51,"lines":203},"fougere.config.ts",[204,207,210,213,215,218,221,224,227],{"text":205,"added":5,"keys":206},"import '@fougere\u002Fadapter-rest';",[],{"text":208,"added":5,"keys":209},"import '@fougere\u002Fadapter-sql';",[],{"text":211,"added":5,"keys":212},"import { defineFougere } from '@fougere\u002Fcore';",[],{"text":51,"added":5,"keys":214},[],{"text":216,"added":5,"keys":217},"export default defineFougere({",[],{"text":219,"added":5,"keys":220},"  protocols: { rest: {} },",[],{"text":222,"added":5,"keys":223},"  sources: { main: { type: 'sqlite', path: '.data\u002Fblog.db' } },",[],{"text":225,"added":5,"keys":226},"  fronds: { '@fougere\u002Flog': {} },",[],{"text":228,"added":5,"keys":229},"});",[],{"path":231,"state":51,"lines":232},"package.json",[233,236,239,242,245,248,251,254,257,260,263,266,269,272,275,278,280,283,286,289,292,295,298,300],{"text":234,"added":5,"keys":235},"{",[],{"text":237,"added":5,"keys":238},"  \"name\": \"my-blog\",",[],{"text":240,"added":5,"keys":241},"  \"private\": true,",[],{"text":243,"added":5,"keys":244},"  \"type\": \"module\",",[],{"text":246,"added":5,"keys":247},"  \"scripts\": {",[],{"text":249,"added":5,"keys":250},"    \"typecheck\": \"tsc\",",[],{"text":252,"added":5,"keys":253},"    \"test\": \"vitest run --config tests\u002Fvitest.config.ts\"",[],{"text":255,"added":5,"keys":256},"  },",[],{"text":258,"added":5,"keys":259},"  \"dependencies\": {",[],{"text":261,"added":5,"keys":262},"    \"@fougere\u002Fadapter-rest\": \"latest\",",[],{"text":264,"added":5,"keys":265},"    \"@fougere\u002Fadapter-sql\": \"latest\",",[],{"text":267,"added":5,"keys":268},"    \"@fougere\u002Fcore\": \"latest\",",[],{"text":270,"added":5,"keys":271},"    \"@fougere\u002Flog\": \"latest\",",[],{"text":273,"added":5,"keys":274},"    \"@fougere\u002Fschema\": \"latest\",",[],{"text":276,"added":5,"keys":277},"    \"better-sqlite3\": \"^13.0.3\"",[],{"text":255,"added":5,"keys":279},[],{"text":281,"added":5,"keys":282},"  \"devDependencies\": {",[],{"text":284,"added":5,"keys":285},"    \"@fougere\u002Fcli\": \"latest\",",[],{"text":287,"added":5,"keys":288},"    \"@fougere\u002Ftesting\": \"latest\",",[],{"text":290,"added":5,"keys":291},"    \"@types\u002Fnode\": \"^22.19.15\",",[],{"text":293,"added":5,"keys":294},"    \"typescript\": \"^7.0.2\",",[],{"text":296,"added":5,"keys":297},"    \"vitest\": \"^4.1.0\"",[],{"text":101,"added":5,"keys":299},[],{"text":130,"added":5,"keys":301},[],"port","We log every mail without touching the handler or the mailer. A class that implements the mailer and asks for it stands in front of it.","md","Around a capability, not inside it.",[307,308,309,310,311,312,313],{"path":55,"state":51},{"path":78,"state":51},{"path":133,"state":134},{"path":171,"state":51},{"path":191,"state":51},{"path":202,"state":51},{"path":231,"state":51},[315,319,322],{"key":151,"side":316,"label":317,"detail":318},"out","AuditMailer → ConsoleMailer","a chain, found by its form",{"key":160,"side":316,"label":320,"detail":321},"a line per mail","under the operation that sent it",{"key":147,"side":323,"label":324,"detail":325},"refuses","two links, no order","refused, naming ports: { Mailer: [ … ] }","Stand *in front* of the port",[328,331],{"title":329,"path":330},"Port","\u002Fdocs\u002Fconcepts\u002Fport",{"title":332,"path":333},"Around every call","\u002Fdocs\u002Fbehaviour\u002Faround-every-call",{},true,{"title":45,"description":303},"5-a-link","tutorial\u002F5-a-link",{"command":340,"said":341,"output":342},"curl -X POST localhost:4100\u002Fapi\u002Fblog\u002Fposts\u002F$ID\u002Fpublish",{"npm":340,"pnpm":340,"bun":340,"yarn":340},"INF [app:blog:AuditMailer] (blog:PostHandler.publish) sending Published: Hi\nmail: Published: Hi","handlers\u002F","yScAuRLDfaLh0q9MI2YTP0fNBf8YZ9Zfy05KBWW1znE",1794396615653]