@afora/mcp 0.0.3 → 0.0.5

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/dist/bin.js CHANGED
@@ -189,7 +189,12 @@ a file of your own.
189
189
  to the server with \`--providers\`), or \`{ "import": "./db.ts#provider" }\` for one created by hand. Options are JSON or
190
190
  \`{ "import": "./db.ts#db" }\` references (any depth); imports are relative to \`output.dir\` and never run by the validator.
191
191
  Omitting \`provider\` is not the same as \`"inMemory"\`: a repository without one uses the application's. For a provider
192
- not created with \`providerOf\`, \`afora_extract\` needs \`imports\` (\`provider\` / \`repositories.<Name>.provider\`).
192
+ not created with \`providerOf\`, \`afora_extract\` needs \`imports\` (\`provider\` / \`repositories.<Name>.provider\` /
193
+ \`providers.<key>\`).
194
+ - Named providers: the top-level \`providers\` is \`{ "<camelCaseKey>": <provider> }\` (generated as \`Providers\`, made by
195
+ \`providersOf\`); a repository or the top-level \`provider\` names one with \`"provider": "<key>"\`, e.g. \`"audit"\`.
196
+ \`"inMemory"\` is not a valid key. Keys of a spec extended directly can be named too. Each application makes its own
197
+ instance of a provider made on start (an \`{ "import" }\` of a \`providerFrom\`), so independent applications do not share it.
193
198
  - Repository \`deletion\`: \`"hard"\` (default) physically removes rows; \`"soft"\` retains \`deletedAt\` and hides deleted rows from reads, counts and relations.
194
199
  - \`relations\` must list exactly one repository for each entity the repository's entity holds, at any depth (inside
195
200
  records, blueprints and nested objects), not through another entity.
@@ -224,7 +229,7 @@ a file of your own.
224
229
 
225
230
  - \`exceptions\`: \`{ layer, message? }\`; \`layer\` is one of \`exceptionLayers\` of \`afora_catalog\`; the message is "<Name>." when omitted.
226
231
  - \`useCases\`: \`input\`, \`response\` and \`context\` (optional) are a blueprint name or \`{ "fields": {...} }\`. \`dependencies\` map a key to a use case, repository
227
- or environment. \`errors\` are declared exceptions or exceptions exported by @afora/core (\`coreExceptions\` of \`afora_catalog\`).
232
+ or environment, or to \`"Logger"\` for the logger of the application (an \`ILogger\`; no step reads it). \`errors\` are declared exceptions or exceptions exported by @afora/core (\`coreExceptions\` of \`afora_catalog\`).
228
233
  - \`implementation\` is \`{ "steps": [...] }\` (generated complete, no hand-written file) or \`{ "import": "../handlers/x.ts#Name" }\`:
229
234
  the generated file has \`abstract class <Name>Base\`; write \`class Name extends <Name>Base\` with \`execute\` at that path.
230
235
  A use case cannot list an imported (hand-written) use case as a dependency.
@@ -334,6 +339,8 @@ var TOP_ORDER = [
334
339
  "extends",
335
340
  "output",
336
341
  "provider",
342
+ "providers",
343
+ "log",
337
344
  ...COLLECTION_KEYS
338
345
  ];
339
346
  function ordered(spec) {
@@ -608,7 +615,7 @@ var tools = [
608
615
  description: "output.dir for the spec; src/domain by default."
609
616
  })),
610
617
  imports: Type.Optional(Type.Record(Type.String(), Type.String(), {
611
- description: 'Where what cannot be described is imported from: a property name -> module, and "./file.ts#export" (relative to output.dir) for a provider not created with providerOf ("provider" or "repositories.<Name>.provider"), a use case with a hand-written execute ("useCases.<Name>") and a run function of a steps use case ("useCases.<Name>.run.<name>"), a command handler ("commands.<Name>.events", "commands.<Name>.onEventError") and a stage written by hand ("outputs.<Name>.pipe.<index>"). Without it extraction fails with an error naming the key.'
618
+ description: 'Where what cannot be described is imported from: a property name -> module, and "./file.ts#export" (relative to output.dir) for a provider not created with providerOf ("provider", "repositories.<Name>.provider" or "providers.<key>" for one of the top-level providers), a use case with a hand-written execute ("useCases.<Name>") and a run function of a steps use case ("useCases.<Name>.run.<name>"), a command handler ("commands.<Name>.events", "commands.<Name>.onEventError") and a stage written by hand ("outputs.<Name>.pipe.<index>"). Without it extraction fails with an error naming the key.'
612
619
  }))
613
620
  }, { additionalProperties: false })),
614
621
  readOnly: true,
@@ -665,7 +672,7 @@ var tools = [
665
672
  description: "PascalCase name, unique across all kinds."
666
673
  },
667
674
  declaration: {
668
- description: 'The declaration. property: { base, with? } | { enum } | { opaque, import }. blueprint/environment: { fields }. record/entity: { fields, unique?, sensitive?, change?, plugins? }. repository: { entity, actions, provider?, deletion?: hard | soft, relations?, environments? }. exception: { layer, message? }. useCase: { description?, tags?, deprecated?, input, response, context?, dependencies?, errors?, implementation: { steps: [...] } | { import: "../handlers/x.ts#Name" } } (see the guide for steps). command: { description?, tags?, context?, useCases: { op: "UseCaseName" }, transaction?, events?: "after-commit" | "manual" | { import }, onEventError?: { import } }. output: { commands: [names], statuses?: { ExceptionName: 400-599 }, pipe: [{ transform, options? } | { sink, options? } | { import }] }. provider: "inMemory" | { use, options? } | { import: "./file.ts#name" } (the application provider is the top-level `provider` of the spec: afora_spec_write).',
675
+ description: 'The declaration. property: { base, with? } | { enum } | { opaque, import }. blueprint/environment: { fields }. record/entity: { fields, unique?, sensitive?, change?, plugins? }. repository: { entity, actions, provider?, deletion?: hard | soft, relations?, environments? }. exception: { layer, message? }. useCase: { description?, tags?, deprecated?, input, response, context?, dependencies?, errors?, implementation: { steps: [...] } | { import: "../handlers/x.ts#Name" } } (see the guide for steps). command: { description?, tags?, context?, useCases: { op: "UseCaseName" }, transaction?, events?: "after-commit" | "manual" | { import }, onEventError?: { import } }. output: { commands: [names], statuses?: { ExceptionName: 400-599 }, pipe: [{ transform, options? } | { sink, options? } | { import }] }. provider: "inMemory" | { use, options? } | { import: "./file.ts#name" } | "<key>" of the top-level `providers` (the application provider and the named `providers` are top-level keys of the spec: afora_spec_write). A use case dependency can also be "Logger"; the top-level `log` is { level?, providers: ["json" | "console" | { import }] }.',
669
676
  anyOf: Object.values(declarationSchemas).map((schema) => json(schema))
670
677
  }
671
678
  },
package/dist/guide.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** A short guide to the afora.json format, for the agent (the resource afora://spec/guide). */
2
- export declare const GUIDE = "# afora.json guide\n\n`afora.json` describes a domain. `afora_generate` turns it into TypeScript on `@afora/core`\n(one file per kind of declaration, under `output.dir`, default `src/domain`). The generated files\nstart with \"// Generated by @afora/codegen from afora.json. Do not edit.\" and are overwritten on\nevery run, so change the JSON, never those files. To add behavior, extend the generated class in\na file of your own.\n\n## Workflow\n\n1. `afora_catalog`: what you can use (Std properties, record plugins, rules).\n2. `afora_spec_read`: the current spec (or the one extracted from the code).\n3. Edit with `afora_spec_upsert` / `afora_spec_remove` (one declaration at a time; the whole\n spec must stay valid, so declare what a declaration uses first) or `afora_spec_write`.\n4. `afora_spec_validate`, then `afora_generate` (`dryRun: true` to preview). The result has the\n TypeScript diagnostics of the generated files: fix the spec until there are none.\n\n## Shape\n\n```json\n{\n \"version\": 1,\n \"name\": \"blog\",\n \"output\": { \"dir\": \"src/domain\" },\n \"provider\": \"inMemory\",\n \"properties\": { \"Age\": { \"base\": \"integer\", \"with\": { \"min\": 0, \"max\": 150 } },\n \"Status\": { \"enum\": [\"draft\", \"published\"] } },\n \"blueprints\": { \"Address\": { \"fields\": { \"street\": \"string\", \"zip\": \"string\" } } },\n \"records\": { \"Money\": { \"fields\": { \"amount\": \"number\", \"currency\": \"string\" } } },\n \"entities\": { \"User\": { \"fields\": { \"name\": \"text\", \"email\": \"email\", \"password\": \"password\",\n \"age\": \"Age?\", \"address\": \"Address\", \"tags\": \"string[]\" },\n \"unique\": [\"email\"], \"change\": [\"name\"] },\n \"Post\": { \"fields\": { \"title\": \"text\", \"status\": \"Status\",\n \"author\": \"User\", \"coauthors\": \"User[]\" } } },\n \"environments\": { \"Database\": { \"fields\": { \"DATABASE_URL\": \"url\", \"PORT\": \"integer\" } } },\n \"repositories\": { \"Users\": { \"entity\": \"User\", \"actions\": [\"findById\", \"findByEmail\", \"create\", \"update\"],\n \"environments\": [\"Database\"] },\n \"Posts\": { \"entity\": \"Post\", \"actions\": [\"findById\", \"findManyByAuthor\", \"create\"],\n \"relations\": [\"Users\"] } }\n}\n```\n\n## Fields\n\n- Shorthand string: `\"email\"`, `\"Age?\"` (optional), `\"string[]\"` (list), `\"User[]?\"` (optional list).\n- Lowercase names are Std properties (see `afora_catalog`: id, slug, email, phone, url, ip, password,\n string, text, number, integer, boolean, date, dateTime...). PascalCase names are declarations of\n the spec (properties, blueprints, records, entities).\n- Object form: `{ \"type\": \"string\", \"optional\": true, \"list\": { \"minItems\": 1, \"maxItems\": 5 }, \"with\": { \"pattern\": \"^a\" } }`.\n `with` applies to each item of a list and only takes options valid for the type (min/max/pattern for strings,\n min/max/exclusiveMin/exclusiveMax/multipleOf for numbers, plus title/description/examples/deprecated/readOnly/writeOnly/default).\n- Enum: `{ \"enum\": [\"a\", \"b\"] }`. Nested object in place: `{ \"fields\": { \"lat\": \"number\", \"lng\": \"number\" } }`.\n- A property of your own: `{ \"base\": \"integer\", \"with\": {...} }`, `{ \"enum\": [...] }`, or\n `{ \"opaque\": true, \"import\": \"./my-property\" }` for a hand-written one (imported as it is).\n\n## Rules the validator enforces\n\n- Declaration names are PascalCase and unique across every kind; names exported by @afora/core are taken.\n- No circular references between declarations (store an id instead of the entity).\n- An entity cannot declare `id`, `createdAt`, `updatedAt`, `deletedAt` (it gets them). No record member names\n (`toJSON`, `set`, `onSet`...); see `reservedRecordKeys` in the catalog. `blueprint` and `options` are valid fields.\n- `unique`, `sensitive`, `change` must name fields (or keys of its plugins); `change` cannot name an identity key.\n `password` is always sensitive and `slug` always unique; `id` is always unique.\n- `plugins`: Std record plugins (publishable, archivable, taggable, ownable, authenticatable); their keys cannot be declared again.\n- Repository actions: `findById`; `findBy<Key>` only for unique keys (id, `unique`, slug fields, unique plugin keys such as\n `email` of authenticatable); `findManyBy<Key>` only for keys that are not unique; `create`, `update`, `delete`, `count`, and the\n queries `findMany`, `exists`, `findPage` (no key needed; `where`, `orderBy`, `limit`, `offset` at runtime).\n- `provider` (top level for the application, and optionally on a repository, which then uses its own): `\"inMemory\"`,\n `{ \"use\": \"<name>\", \"options\": {...} }` with a name from the `providers` of `afora_catalog` (external ones are given\n to the server with `--providers`), or `{ \"import\": \"./db.ts#provider\" }` for one created by hand. Options are JSON or\n `{ \"import\": \"./db.ts#db\" }` references (any depth); imports are relative to `output.dir` and never run by the validator.\n Omitting `provider` is not the same as `\"inMemory\"`: a repository without one uses the application's. For a provider\n not created with `providerOf`, `afora_extract` needs `imports` (`provider` / `repositories.<Name>.provider`).\n- Repository `deletion`: `\"hard\"` (default) physically removes rows; `\"soft\"` retains `deletedAt` and hides deleted rows from reads, counts and relations.\n- `relations` must list exactly one repository for each entity the repository's entity holds, at any depth (inside\n records, blueprints and nested objects), not through another entity.\n- Environments hold properties only (no lists, no objects).\n- `extends`: other specs (relative path to their afora.json, or a package) whose declarations you can reference;\n the same name for different declarations is a conflict.\n\n## Exceptions and use cases\n\n```json\n{\n \"exceptions\": { \"UserAlreadyExists\": { \"layer\": \"use-case\", \"message\": \"A user with this email already exists.\" } },\n \"useCases\": {\n \"Register\": {\n \"description\": \"Signs a user up.\", \"tags\": [\"users\"],\n \"input\": { \"fields\": { \"name\": \"text\", \"email\": \"email\", \"plainPassword\": \"string\" } },\n \"response\": { \"fields\": { \"user\": \"User\" } },\n \"dependencies\": { \"users\": \"Users\" },\n \"errors\": [\"UserAlreadyExists\", \"RepositoryUniqueViolation\"],\n \"implementation\": { \"steps\": [\n { \"find\": \"users.findByEmail\", \"args\": [\"$input.email\"], \"as\": \"existing\" },\n { \"ensure\": \"absent\", \"value\": \"$existing\", \"error\": \"UserAlreadyExists\" },\n { \"run\": \"hashPassword\", \"fn\": { \"import\": \"../hash#hashPassword\" }, \"args\": [\"$input.plainPassword\"], \"as\": \"hash\" },\n { \"build\": \"User\", \"values\": { \"name\": \"$input.name\", \"email\": \"$input.email\", \"password\": \"$hash\" }, \"as\": \"user\" },\n { \"create\": \"users\", \"value\": \"$user\", \"as\": \"saved\" },\n { \"return\": { \"user\": \"$saved\" } }\n ] }\n }\n }\n}\n```\n\n- `exceptions`: `{ layer, message? }`; `layer` is one of `exceptionLayers` of `afora_catalog`; the message is \"<Name>.\" when omitted.\n- `useCases`: `input`, `response` and `context` (optional) are a blueprint name or `{ \"fields\": {...} }`. `dependencies` map a key to a use case, repository\n or environment. `errors` are declared exceptions or exceptions exported by @afora/core (`coreExceptions` of `afora_catalog`).\n- `implementation` is `{ \"steps\": [...] }` (generated complete, no hand-written file) or `{ \"import\": \"../handlers/x.ts#Name\" }`:\n the generated file has `abstract class <Name>Base`; write `class Name extends <Name>Base` with `execute` at that path.\n A use case cannot list an imported (hand-written) use case as a dependency.\n- Steps run in order and `return` is last, exactly once: `find` (\"<repo>.<read action>\"), `ensure` (exists | absent | equals | notEquals | true | false;\n `other` for the comparisons, `error` a declared exception name or a core one, listed in `errors`; optional `details`), `build` (a record or\n entity name, `values`, `as`), `change` ($ref, `key` in the class's change keys, `value`), `invoke` ($ref, `method`, `args`),\n `create` / `update` / `delete` (repository key, `value`), `call` (use case key, `input`) and `run` (a unique name, `fn` is\n `{ \"import\": \"../x.ts#fn\" }`, `args`; the function gets (...args, { context })). `as` binds the result.\n- Values: `\"$input.key\"`, `\"$context.key\"`, `\"$env.<dependency>.VAR\"`, `\"$name.path.0\"` (a bound result); a literal; `{ \"literal\": \"$5\" }` for a string that\n starts with $; or an object/array of values. No expressions: compute with `run`.\n- `afora_extract` needs `imports` for `useCases.<Name>` (hand-written execute) and `useCases.<Name>.run.<name>` (run functions).\n\n## Commands and outputs\n\n```json\n{\n \"commands\": {\n \"Users\": {\n \"description\": \"Accounts.\", \"tags\": [\"public\"],\n \"context\": { \"fields\": { \"userId\": \"id?\" } },\n \"useCases\": { \"register\": \"Register\" },\n \"transaction\": true\n }\n },\n \"outputs\": {\n \"Api\": {\n \"commands\": [\"Users\"],\n \"statuses\": { \"UserAlreadyExists\": 409 },\n \"pipe\": [\n { \"transform\": \"select\", \"options\": { \"tags\": [\"public\"] } },\n { \"sink\": \"openapi\", \"options\": { \"info\": { \"title\": \"Blog\", \"version\": \"1.0.0\" } } }\n ]\n }\n }\n}\n```\n\n- `commands`: `useCases` maps an operation name (an identifier) to a declared use case; `context` is a blueprint name or `{ \"fields\": {...} }`.\n `transaction: true` runs each call in a transaction of the repositories' provider: it is checked when the code runs, not by the validator\n (the provider is unknown then). `events` is `\"after-commit\"` (default), `\"manual\"` or `{ \"import\": \"./events.ts#dispatch\" }` (a handler);\n `onEventError` is an import and only with `after-commit`. Defaults (`transaction: false`, `events: \"after-commit\"`, no tags) are not written.\n- `outputs`: `commands` are declared commands; `statuses` maps an exception name (declared or exported by @afora/core) to a status from 400 to 599;\n `pipe` is transforms and, last, at most one sink, each `{ \"transform\" | \"sink\": name, \"options\"?: {...JSON} }` with a name from the `stages` of\n `afora_catalog` (`select`, `openapi`, plus the ones given to the server with `--stages`), or `{ \"import\": \"./stages.ts#auth\" }` for a stage written by hand.\n- `afora_extract` needs `imports` for `commands.<Name>.events`, `commands.<Name>.onEventError` and `outputs.<Name>.pipe.<index>` (hand-written ones).\n";
2
+ export declare const GUIDE = "# afora.json guide\n\n`afora.json` describes a domain. `afora_generate` turns it into TypeScript on `@afora/core`\n(one file per kind of declaration, under `output.dir`, default `src/domain`). The generated files\nstart with \"// Generated by @afora/codegen from afora.json. Do not edit.\" and are overwritten on\nevery run, so change the JSON, never those files. To add behavior, extend the generated class in\na file of your own.\n\n## Workflow\n\n1. `afora_catalog`: what you can use (Std properties, record plugins, rules).\n2. `afora_spec_read`: the current spec (or the one extracted from the code).\n3. Edit with `afora_spec_upsert` / `afora_spec_remove` (one declaration at a time; the whole\n spec must stay valid, so declare what a declaration uses first) or `afora_spec_write`.\n4. `afora_spec_validate`, then `afora_generate` (`dryRun: true` to preview). The result has the\n TypeScript diagnostics of the generated files: fix the spec until there are none.\n\n## Shape\n\n```json\n{\n \"version\": 1,\n \"name\": \"blog\",\n \"output\": { \"dir\": \"src/domain\" },\n \"provider\": \"inMemory\",\n \"properties\": { \"Age\": { \"base\": \"integer\", \"with\": { \"min\": 0, \"max\": 150 } },\n \"Status\": { \"enum\": [\"draft\", \"published\"] } },\n \"blueprints\": { \"Address\": { \"fields\": { \"street\": \"string\", \"zip\": \"string\" } } },\n \"records\": { \"Money\": { \"fields\": { \"amount\": \"number\", \"currency\": \"string\" } } },\n \"entities\": { \"User\": { \"fields\": { \"name\": \"text\", \"email\": \"email\", \"password\": \"password\",\n \"age\": \"Age?\", \"address\": \"Address\", \"tags\": \"string[]\" },\n \"unique\": [\"email\"], \"change\": [\"name\"] },\n \"Post\": { \"fields\": { \"title\": \"text\", \"status\": \"Status\",\n \"author\": \"User\", \"coauthors\": \"User[]\" } } },\n \"environments\": { \"Database\": { \"fields\": { \"DATABASE_URL\": \"url\", \"PORT\": \"integer\" } } },\n \"repositories\": { \"Users\": { \"entity\": \"User\", \"actions\": [\"findById\", \"findByEmail\", \"create\", \"update\"],\n \"environments\": [\"Database\"] },\n \"Posts\": { \"entity\": \"Post\", \"actions\": [\"findById\", \"findManyByAuthor\", \"create\"],\n \"relations\": [\"Users\"] } }\n}\n```\n\n## Fields\n\n- Shorthand string: `\"email\"`, `\"Age?\"` (optional), `\"string[]\"` (list), `\"User[]?\"` (optional list).\n- Lowercase names are Std properties (see `afora_catalog`: id, slug, email, phone, url, ip, password,\n string, text, number, integer, boolean, date, dateTime...). PascalCase names are declarations of\n the spec (properties, blueprints, records, entities).\n- Object form: `{ \"type\": \"string\", \"optional\": true, \"list\": { \"minItems\": 1, \"maxItems\": 5 }, \"with\": { \"pattern\": \"^a\" } }`.\n `with` applies to each item of a list and only takes options valid for the type (min/max/pattern for strings,\n min/max/exclusiveMin/exclusiveMax/multipleOf for numbers, plus title/description/examples/deprecated/readOnly/writeOnly/default).\n- Enum: `{ \"enum\": [\"a\", \"b\"] }`. Nested object in place: `{ \"fields\": { \"lat\": \"number\", \"lng\": \"number\" } }`.\n- A property of your own: `{ \"base\": \"integer\", \"with\": {...} }`, `{ \"enum\": [...] }`, or\n `{ \"opaque\": true, \"import\": \"./my-property\" }` for a hand-written one (imported as it is).\n\n## Rules the validator enforces\n\n- Declaration names are PascalCase and unique across every kind; names exported by @afora/core are taken.\n- No circular references between declarations (store an id instead of the entity).\n- An entity cannot declare `id`, `createdAt`, `updatedAt`, `deletedAt` (it gets them). No record member names\n (`toJSON`, `set`, `onSet`...); see `reservedRecordKeys` in the catalog. `blueprint` and `options` are valid fields.\n- `unique`, `sensitive`, `change` must name fields (or keys of its plugins); `change` cannot name an identity key.\n `password` is always sensitive and `slug` always unique; `id` is always unique.\n- `plugins`: Std record plugins (publishable, archivable, taggable, ownable, authenticatable); their keys cannot be declared again.\n- Repository actions: `findById`; `findBy<Key>` only for unique keys (id, `unique`, slug fields, unique plugin keys such as\n `email` of authenticatable); `findManyBy<Key>` only for keys that are not unique; `create`, `update`, `delete`, `count`, and the\n queries `findMany`, `exists`, `findPage` (no key needed; `where`, `orderBy`, `limit`, `offset` at runtime).\n- `provider` (top level for the application, and optionally on a repository, which then uses its own): `\"inMemory\"`,\n `{ \"use\": \"<name>\", \"options\": {...} }` with a name from the `providers` of `afora_catalog` (external ones are given\n to the server with `--providers`), or `{ \"import\": \"./db.ts#provider\" }` for one created by hand. Options are JSON or\n `{ \"import\": \"./db.ts#db\" }` references (any depth); imports are relative to `output.dir` and never run by the validator.\n Omitting `provider` is not the same as `\"inMemory\"`: a repository without one uses the application's. For a provider\n not created with `providerOf`, `afora_extract` needs `imports` (`provider` / `repositories.<Name>.provider` /\n `providers.<key>`).\n- Named providers: the top-level `providers` is `{ \"<camelCaseKey>\": <provider> }` (generated as `Providers`, made by\n `providersOf`); a repository or the top-level `provider` names one with `\"provider\": \"<key>\"`, e.g. `\"audit\"`.\n `\"inMemory\"` is not a valid key. Keys of a spec extended directly can be named too. Each application makes its own\n instance of a provider made on start (an `{ \"import\" }` of a `providerFrom`), so independent applications do not share it.\n- Repository `deletion`: `\"hard\"` (default) physically removes rows; `\"soft\"` retains `deletedAt` and hides deleted rows from reads, counts and relations.\n- `relations` must list exactly one repository for each entity the repository's entity holds, at any depth (inside\n records, blueprints and nested objects), not through another entity.\n- Environments hold properties only (no lists, no objects).\n- `extends`: other specs (relative path to their afora.json, or a package) whose declarations you can reference;\n the same name for different declarations is a conflict.\n\n## Exceptions and use cases\n\n```json\n{\n \"exceptions\": { \"UserAlreadyExists\": { \"layer\": \"use-case\", \"message\": \"A user with this email already exists.\" } },\n \"useCases\": {\n \"Register\": {\n \"description\": \"Signs a user up.\", \"tags\": [\"users\"],\n \"input\": { \"fields\": { \"name\": \"text\", \"email\": \"email\", \"plainPassword\": \"string\" } },\n \"response\": { \"fields\": { \"user\": \"User\" } },\n \"dependencies\": { \"users\": \"Users\" },\n \"errors\": [\"UserAlreadyExists\", \"RepositoryUniqueViolation\"],\n \"implementation\": { \"steps\": [\n { \"find\": \"users.findByEmail\", \"args\": [\"$input.email\"], \"as\": \"existing\" },\n { \"ensure\": \"absent\", \"value\": \"$existing\", \"error\": \"UserAlreadyExists\" },\n { \"run\": \"hashPassword\", \"fn\": { \"import\": \"../hash#hashPassword\" }, \"args\": [\"$input.plainPassword\"], \"as\": \"hash\" },\n { \"build\": \"User\", \"values\": { \"name\": \"$input.name\", \"email\": \"$input.email\", \"password\": \"$hash\" }, \"as\": \"user\" },\n { \"create\": \"users\", \"value\": \"$user\", \"as\": \"saved\" },\n { \"return\": { \"user\": \"$saved\" } }\n ] }\n }\n }\n}\n```\n\n- `exceptions`: `{ layer, message? }`; `layer` is one of `exceptionLayers` of `afora_catalog`; the message is \"<Name>.\" when omitted.\n- `useCases`: `input`, `response` and `context` (optional) are a blueprint name or `{ \"fields\": {...} }`. `dependencies` map a key to a use case, repository\n or environment, or to `\"Logger\"` for the logger of the application (an `ILogger`; no step reads it). `errors` are declared exceptions or exceptions exported by @afora/core (`coreExceptions` of `afora_catalog`).\n- `implementation` is `{ \"steps\": [...] }` (generated complete, no hand-written file) or `{ \"import\": \"../handlers/x.ts#Name\" }`:\n the generated file has `abstract class <Name>Base`; write `class Name extends <Name>Base` with `execute` at that path.\n A use case cannot list an imported (hand-written) use case as a dependency.\n- Steps run in order and `return` is last, exactly once: `find` (\"<repo>.<read action>\"), `ensure` (exists | absent | equals | notEquals | true | false;\n `other` for the comparisons, `error` a declared exception name or a core one, listed in `errors`; optional `details`), `build` (a record or\n entity name, `values`, `as`), `change` ($ref, `key` in the class's change keys, `value`), `invoke` ($ref, `method`, `args`),\n `create` / `update` / `delete` (repository key, `value`), `call` (use case key, `input`) and `run` (a unique name, `fn` is\n `{ \"import\": \"../x.ts#fn\" }`, `args`; the function gets (...args, { context })). `as` binds the result.\n- Values: `\"$input.key\"`, `\"$context.key\"`, `\"$env.<dependency>.VAR\"`, `\"$name.path.0\"` (a bound result); a literal; `{ \"literal\": \"$5\" }` for a string that\n starts with $; or an object/array of values. No expressions: compute with `run`.\n- `afora_extract` needs `imports` for `useCases.<Name>` (hand-written execute) and `useCases.<Name>.run.<name>` (run functions).\n\n## Commands and outputs\n\n```json\n{\n \"commands\": {\n \"Users\": {\n \"description\": \"Accounts.\", \"tags\": [\"public\"],\n \"context\": { \"fields\": { \"userId\": \"id?\" } },\n \"useCases\": { \"register\": \"Register\" },\n \"transaction\": true\n }\n },\n \"outputs\": {\n \"Api\": {\n \"commands\": [\"Users\"],\n \"statuses\": { \"UserAlreadyExists\": 409 },\n \"pipe\": [\n { \"transform\": \"select\", \"options\": { \"tags\": [\"public\"] } },\n { \"sink\": \"openapi\", \"options\": { \"info\": { \"title\": \"Blog\", \"version\": \"1.0.0\" } } }\n ]\n }\n }\n}\n```\n\n- `commands`: `useCases` maps an operation name (an identifier) to a declared use case; `context` is a blueprint name or `{ \"fields\": {...} }`.\n `transaction: true` runs each call in a transaction of the repositories' provider: it is checked when the code runs, not by the validator\n (the provider is unknown then). `events` is `\"after-commit\"` (default), `\"manual\"` or `{ \"import\": \"./events.ts#dispatch\" }` (a handler);\n `onEventError` is an import and only with `after-commit`. Defaults (`transaction: false`, `events: \"after-commit\"`, no tags) are not written.\n- `outputs`: `commands` are declared commands; `statuses` maps an exception name (declared or exported by @afora/core) to a status from 400 to 599;\n `pipe` is transforms and, last, at most one sink, each `{ \"transform\" | \"sink\": name, \"options\"?: {...JSON} }` with a name from the `stages` of\n `afora_catalog` (`select`, `openapi`, plus the ones given to the server with `--stages`), or `{ \"import\": \"./stages.ts#auth\" }` for a stage written by hand.\n- `afora_extract` needs `imports` for `commands.<Name>.events`, `commands.<Name>.onEventError` and `outputs.<Name>.pipe.<index>` (hand-written ones).\n";
package/dist/index.js CHANGED
@@ -71,7 +71,12 @@ a file of your own.
71
71
  to the server with \`--providers\`), or \`{ "import": "./db.ts#provider" }\` for one created by hand. Options are JSON or
72
72
  \`{ "import": "./db.ts#db" }\` references (any depth); imports are relative to \`output.dir\` and never run by the validator.
73
73
  Omitting \`provider\` is not the same as \`"inMemory"\`: a repository without one uses the application's. For a provider
74
- not created with \`providerOf\`, \`afora_extract\` needs \`imports\` (\`provider\` / \`repositories.<Name>.provider\`).
74
+ not created with \`providerOf\`, \`afora_extract\` needs \`imports\` (\`provider\` / \`repositories.<Name>.provider\` /
75
+ \`providers.<key>\`).
76
+ - Named providers: the top-level \`providers\` is \`{ "<camelCaseKey>": <provider> }\` (generated as \`Providers\`, made by
77
+ \`providersOf\`); a repository or the top-level \`provider\` names one with \`"provider": "<key>"\`, e.g. \`"audit"\`.
78
+ \`"inMemory"\` is not a valid key. Keys of a spec extended directly can be named too. Each application makes its own
79
+ instance of a provider made on start (an \`{ "import" }\` of a \`providerFrom\`), so independent applications do not share it.
75
80
  - Repository \`deletion\`: \`"hard"\` (default) physically removes rows; \`"soft"\` retains \`deletedAt\` and hides deleted rows from reads, counts and relations.
76
81
  - \`relations\` must list exactly one repository for each entity the repository's entity holds, at any depth (inside
77
82
  records, blueprints and nested objects), not through another entity.
@@ -106,7 +111,7 @@ a file of your own.
106
111
 
107
112
  - \`exceptions\`: \`{ layer, message? }\`; \`layer\` is one of \`exceptionLayers\` of \`afora_catalog\`; the message is "<Name>." when omitted.
108
113
  - \`useCases\`: \`input\`, \`response\` and \`context\` (optional) are a blueprint name or \`{ "fields": {...} }\`. \`dependencies\` map a key to a use case, repository
109
- or environment. \`errors\` are declared exceptions or exceptions exported by @afora/core (\`coreExceptions\` of \`afora_catalog\`).
114
+ or environment, or to \`"Logger"\` for the logger of the application (an \`ILogger\`; no step reads it). \`errors\` are declared exceptions or exceptions exported by @afora/core (\`coreExceptions\` of \`afora_catalog\`).
110
115
  - \`implementation\` is \`{ "steps": [...] }\` (generated complete, no hand-written file) or \`{ "import": "../handlers/x.ts#Name" }\`:
111
116
  the generated file has \`abstract class <Name>Base\`; write \`class Name extends <Name>Base\` with \`execute\` at that path.
112
117
  A use case cannot list an imported (hand-written) use case as a dependency.
@@ -324,6 +329,8 @@ var TOP_ORDER = [
324
329
  "extends",
325
330
  "output",
326
331
  "provider",
332
+ "providers",
333
+ "log",
327
334
  ...COLLECTION_KEYS
328
335
  ];
329
336
  function ordered(spec) {
@@ -598,7 +605,7 @@ var tools = [
598
605
  description: "output.dir for the spec; src/domain by default."
599
606
  })),
600
607
  imports: Type.Optional(Type.Record(Type.String(), Type.String(), {
601
- description: 'Where what cannot be described is imported from: a property name -> module, and "./file.ts#export" (relative to output.dir) for a provider not created with providerOf ("provider" or "repositories.<Name>.provider"), a use case with a hand-written execute ("useCases.<Name>") and a run function of a steps use case ("useCases.<Name>.run.<name>"), a command handler ("commands.<Name>.events", "commands.<Name>.onEventError") and a stage written by hand ("outputs.<Name>.pipe.<index>"). Without it extraction fails with an error naming the key.'
608
+ description: 'Where what cannot be described is imported from: a property name -> module, and "./file.ts#export" (relative to output.dir) for a provider not created with providerOf ("provider", "repositories.<Name>.provider" or "providers.<key>" for one of the top-level providers), a use case with a hand-written execute ("useCases.<Name>") and a run function of a steps use case ("useCases.<Name>.run.<name>"), a command handler ("commands.<Name>.events", "commands.<Name>.onEventError") and a stage written by hand ("outputs.<Name>.pipe.<index>"). Without it extraction fails with an error naming the key.'
602
609
  }))
603
610
  }, { additionalProperties: false })),
604
611
  readOnly: true,
@@ -655,7 +662,7 @@ var tools = [
655
662
  description: "PascalCase name, unique across all kinds."
656
663
  },
657
664
  declaration: {
658
- description: 'The declaration. property: { base, with? } | { enum } | { opaque, import }. blueprint/environment: { fields }. record/entity: { fields, unique?, sensitive?, change?, plugins? }. repository: { entity, actions, provider?, deletion?: hard | soft, relations?, environments? }. exception: { layer, message? }. useCase: { description?, tags?, deprecated?, input, response, context?, dependencies?, errors?, implementation: { steps: [...] } | { import: "../handlers/x.ts#Name" } } (see the guide for steps). command: { description?, tags?, context?, useCases: { op: "UseCaseName" }, transaction?, events?: "after-commit" | "manual" | { import }, onEventError?: { import } }. output: { commands: [names], statuses?: { ExceptionName: 400-599 }, pipe: [{ transform, options? } | { sink, options? } | { import }] }. provider: "inMemory" | { use, options? } | { import: "./file.ts#name" } (the application provider is the top-level `provider` of the spec: afora_spec_write).',
665
+ description: 'The declaration. property: { base, with? } | { enum } | { opaque, import }. blueprint/environment: { fields }. record/entity: { fields, unique?, sensitive?, change?, plugins? }. repository: { entity, actions, provider?, deletion?: hard | soft, relations?, environments? }. exception: { layer, message? }. useCase: { description?, tags?, deprecated?, input, response, context?, dependencies?, errors?, implementation: { steps: [...] } | { import: "../handlers/x.ts#Name" } } (see the guide for steps). command: { description?, tags?, context?, useCases: { op: "UseCaseName" }, transaction?, events?: "after-commit" | "manual" | { import }, onEventError?: { import } }. output: { commands: [names], statuses?: { ExceptionName: 400-599 }, pipe: [{ transform, options? } | { sink, options? } | { import }] }. provider: "inMemory" | { use, options? } | { import: "./file.ts#name" } | "<key>" of the top-level `providers` (the application provider and the named `providers` are top-level keys of the spec: afora_spec_write). A use case dependency can also be "Logger"; the top-level `log` is { level?, providers: ["json" | "console" | { import }] }.',
659
666
  anyOf: Object.values(declarationSchemas).map((schema) => json(schema))
660
667
  }
661
668
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afora/mcp",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "An MCP server that lets an agent discover the afora catalog, edit afora.json, validate it, generate the code and check the result.",
5
5
  "keywords": [
6
6
  "afora",
@@ -49,7 +49,7 @@
49
49
  "test": "bun test"
50
50
  },
51
51
  "dependencies": {
52
- "@afora/codegen": "^0.0.3",
52
+ "@afora/codegen": "^0.0.5",
53
53
  "@modelcontextprotocol/sdk": "^1.32.0"
54
54
  },
55
55
  "devDependencies": {