[{"data":1,"prerenderedAt":310},["ShallowReactive",2],{"fougere:post.list:{}":3,"tutorial-steps":6,"tutorial:\u002Ftutorial\u002F4-a-port":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":271,"description":272,"extension":273,"fig":274,"files":275,"grows":282,"heading":295,"install":296,"lede":272,"links":297,"meta":301,"name":26,"navigation":89,"path":24,"seo":302,"slug":303,"stem":304,"step":25,"tryIt":305,"zero":296,"__hash__":309},"tutorial\u002Ftutorial\u002F4-a-port.md","Mail the author, without naming a provider","Add FakeMailer extends Mailer and the app refuses to start, naming both classes and the line that settles it: ports: { Mailer: '…' }.",{"type":48,"value":49,"toc":50},"minimark",[],{"title":51,"searchDepth":17,"depth":17,"links":52},"",[],[54,77,134,158,170,199],{"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":79,"lines":80},"fronds\u002Fblog\u002Fhandlers\u002FPostHandler.ts","changed",[81,84,87,91,93,96,99,102,105,107,110,113,116,119,121,124,126,129,131],{"text":82,"added":5,"keys":83},"import { ErrorCode, FougereError, Repository } from '@fougere\u002Fcore';",[],{"text":85,"added":5,"keys":86},"import Post from '..\u002Fentities\u002FPost.js';",[],{"text":88,"added":89,"keys":90},"import Mailer from '..\u002Fservices\u002FMailer.js';",true,[],{"text":51,"added":5,"keys":92},[],{"text":94,"added":5,"keys":95},"export default class PostHandler extends Repository\u003CPost> {",[],{"text":97,"added":89,"keys":98},"  constructor(private mailer: Mailer) {",[],{"text":100,"added":89,"keys":101},"    super();",[],{"text":103,"added":89,"keys":104},"  }",[],{"text":51,"added":89,"keys":106},[],{"text":108,"added":5,"keys":109},"  async publish(id: Post['id']): Promise\u003CPost> {",[],{"text":111,"added":5,"keys":112},"    const post = await this.findById(id);",[],{"text":114,"added":5,"keys":115},"    if (!post) throw new FougereError({ code: ErrorCode.NOT_FOUND, message: 'No such post' });",[],{"text":117,"added":5,"keys":118},"    if (post.status === 'published') throw new FougereError({ code: ErrorCode.CONFLICT, message: 'Already published' });",[],{"text":51,"added":5,"keys":120},[],{"text":122,"added":89,"keys":123},"    await this.mailer.send(`Published: ${post.title}`);",[],{"text":51,"added":89,"keys":125},[],{"text":127,"added":5,"keys":128},"    return this.update(id, { status: 'published' });",[],{"text":103,"added":5,"keys":130},[],{"text":132,"added":5,"keys":133},"}",[],{"path":135,"state":136,"lines":137},"fronds\u002Fblog\u002Fservices\u002FConsoleMailer.ts","new",[138,141,143,148,151,154,156],{"text":139,"added":5,"keys":140},"import Mailer from '.\u002FMailer.js';",[],{"text":51,"added":5,"keys":142},[],{"text":144,"added":5,"keys":145},"export default class ConsoleMailer extends Mailer {",[146,147],"b","c",{"text":149,"added":5,"keys":150},"  async send(subject: string): Promise\u003Cvoid> {",[],{"text":152,"added":5,"keys":153},"    console.log(`mail: ${subject}`);",[],{"text":103,"added":5,"keys":155},[],{"text":132,"added":5,"keys":157},[],{"path":159,"state":136,"lines":160},"fronds\u002Fblog\u002Fservices\u002FMailer.ts",[161,165,168],{"text":162,"added":5,"keys":163},"export default abstract class Mailer {",[164],"a",{"text":166,"added":5,"keys":167},"  abstract send(subject: string): Promise\u003Cvoid>;",[],{"text":132,"added":5,"keys":169},[],{"path":171,"state":51,"lines":172},"fougere.config.ts",[173,176,179,182,184,187,190,193,196],{"text":174,"added":5,"keys":175},"import '@fougere\u002Fadapter-rest';",[],{"text":177,"added":5,"keys":178},"import '@fougere\u002Fadapter-sql';",[],{"text":180,"added":5,"keys":181},"import { defineFougere } from '@fougere\u002Fcore';",[],{"text":51,"added":5,"keys":183},[],{"text":185,"added":5,"keys":186},"export default defineFougere({",[],{"text":188,"added":5,"keys":189},"  protocols: { rest: {} },",[],{"text":191,"added":5,"keys":192},"  sources: { main: { type: 'sqlite', path: '.data\u002Fblog.db' } },",[],{"text":194,"added":5,"keys":195},"  fronds: { '@fougere\u002Flog': {} },",[],{"text":197,"added":5,"keys":198},"});",[],{"path":200,"state":51,"lines":201},"package.json",[202,205,208,211,214,217,220,223,226,229,232,235,238,241,244,247,249,252,255,258,261,264,267,269],{"text":203,"added":5,"keys":204},"{",[],{"text":206,"added":5,"keys":207},"  \"name\": \"my-blog\",",[],{"text":209,"added":5,"keys":210},"  \"private\": true,",[],{"text":212,"added":5,"keys":213},"  \"type\": \"module\",",[],{"text":215,"added":5,"keys":216},"  \"scripts\": {",[],{"text":218,"added":5,"keys":219},"    \"typecheck\": \"tsc\",",[],{"text":221,"added":5,"keys":222},"    \"test\": \"vitest run --config tests\u002Fvitest.config.ts\"",[],{"text":224,"added":5,"keys":225},"  },",[],{"text":227,"added":5,"keys":228},"  \"dependencies\": {",[],{"text":230,"added":5,"keys":231},"    \"@fougere\u002Fadapter-rest\": \"latest\",",[],{"text":233,"added":5,"keys":234},"    \"@fougere\u002Fadapter-sql\": \"latest\",",[],{"text":236,"added":5,"keys":237},"    \"@fougere\u002Fcore\": \"latest\",",[],{"text":239,"added":5,"keys":240},"    \"@fougere\u002Flog\": \"latest\",",[],{"text":242,"added":5,"keys":243},"    \"@fougere\u002Fschema\": \"latest\",",[],{"text":245,"added":5,"keys":246},"    \"better-sqlite3\": \"^13.0.3\"",[],{"text":224,"added":5,"keys":248},[],{"text":250,"added":5,"keys":251},"  \"devDependencies\": {",[],{"text":253,"added":5,"keys":254},"    \"@fougere\u002Fcli\": \"latest\",",[],{"text":256,"added":5,"keys":257},"    \"@fougere\u002Ftesting\": \"latest\",",[],{"text":259,"added":5,"keys":260},"    \"@types\u002Fnode\": \"^22.19.15\",",[],{"text":262,"added":5,"keys":263},"    \"typescript\": \"^7.0.2\",",[],{"text":265,"added":5,"keys":266},"    \"vitest\": \"^4.1.0\"",[],{"text":103,"added":5,"keys":268},[],{"text":132,"added":5,"keys":270},[],"port","We mail the author without naming a provider. The handler asks for a mailer, and a class that extends it is enough to provide one.","md","The handler names the capability; the config, if ever needed, names who answers.",[276,277,278,279,280,281],{"path":55,"state":51},{"path":78,"state":79},{"path":135,"state":136},{"path":159,"state":136},{"path":171,"state":51},{"path":200,"state":51},[283,287,291],{"key":164,"side":284,"label":285,"detail":286},"in","stubOf(Mailer)","a double, for a test",{"key":146,"side":288,"label":289,"detail":290},"out","Mailer → ConsoleMailer","bound by its extends",{"key":147,"side":292,"label":293,"detail":294},"refuses","port-implemented-twice","a second class extending Mailer: the boot names both, and the line to write","Mail the author, *without naming a provider*",null,[298],{"title":299,"path":300},"Port","\u002Fdocs\u002Fconcepts\u002Fport",{},{"title":45,"description":272},"4-a-port","tutorial\u002F4-a-port",{"command":306,"said":307,"output":308},"curl -X POST localhost:4100\u002Fapi\u002Fblog\u002Fposts\u002F$ID\u002Fpublish",{"npm":306,"pnpm":306,"bun":306,"yarn":306},"mail: Published: Hi","uOvjnZRSMlpZdJeDkaZPsxXThQKQgkbYRatAh3oJr0o",1794396615604]