@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 +11 -4
- package/dist/guide.d.ts +1 -1
- package/dist/index.js +11 -4
- package/package.json +2 -2
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"
|
|
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
|
|
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"
|
|
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
|
|
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
|
+
"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.
|
|
52
|
+
"@afora/codegen": "^0.0.5",
|
|
53
53
|
"@modelcontextprotocol/sdk": "^1.32.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|