@aventara/client 0.1.0-pilot.4 → 1.0.0-rc.1
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/CHANGELOG.md +679 -0
- package/README.md +168 -132
- package/dist/cli/command.parser.d.ts +6 -4
- package/dist/cli/command.parser.js +22 -11
- package/dist/cli/generate.command.d.ts +6 -1
- package/dist/cli/generate.command.js +12 -1
- package/dist/cli/generation-failure.renderer.js +24 -24
- package/dist/cli/step.reporter.d.ts +31 -0
- package/dist/cli/step.reporter.js +87 -0
- package/dist/cli.js +2 -0
- package/dist/config/client-config.interface.d.ts +7 -7
- package/dist/config/config.loader.d.ts +4 -4
- package/dist/config/config.loader.js +7 -7
- package/dist/config/config.resolver.d.ts +7 -7
- package/dist/config/config.resolver.js +12 -12
- package/dist/config/env.cascade.d.ts +2 -2
- package/dist/config/env.cascade.js +4 -4
- package/dist/config/module-style.resolver.d.ts +1 -1
- package/dist/config/module-style.resolver.js +2 -2
- package/dist/config/tsconfig.locator.d.ts +1 -1
- package/dist/config/tsconfig.locator.js +2 -2
- package/dist/contract/contract.acceptance.d.ts +4 -4
- package/dist/contract/contract.acceptance.js +4 -4
- package/dist/contract/contract.fetcher.d.ts +3 -3
- package/dist/contract/contract.fetcher.js +6 -6
- package/dist/contract/contract.loader.d.ts +6 -6
- package/dist/contract/contract.loader.js +2 -2
- package/dist/emit/client-surface.emitter.d.ts +2 -2
- package/dist/emit/client-surface.emitter.js +77 -43
- package/dist/emit/client-tree.emitter.d.ts +3 -3
- package/dist/emit/client-tree.emitter.js +8 -5
- package/dist/emit/contract-carrier.emitter.d.ts +3 -3
- package/dist/emit/contract-carrier.emitter.js +1 -1
- package/dist/emit/derivation.emitter.js +1 -0
- package/dist/emit/descriptor.emitter.d.ts +2 -2
- package/dist/emit/descriptor.emitter.js +2 -2
- package/dist/emit/emitted-tree.interface.d.ts +1 -1
- package/dist/emit/enum.emitter.d.ts +2 -2
- package/dist/emit/field-check.emitter.d.ts +21 -0
- package/dist/emit/field-check.emitter.js +603 -0
- package/dist/emit/json-shape.emitter.d.ts +16 -0
- package/dist/emit/json-shape.emitter.js +88 -0
- package/dist/emit/name.deriver.d.ts +6 -6
- package/dist/emit/name.deriver.js +15 -15
- package/dist/emit/named-type.emitter.d.ts +2 -2
- package/dist/emit/runtime.emitter.d.ts +2 -2
- package/dist/emit/runtime.emitter.js +117 -112
- package/dist/emit/scalar.codec.d.ts +1 -1
- package/dist/emit/scalar.codec.js +78 -35
- package/dist/emit/transaction.emitter.js +131 -83
- package/dist/generate.d.ts +11 -3
- package/dist/generate.js +40 -26
- package/dist/index.d.ts +1 -1
- package/dist/init/client-init.errors.d.ts +4 -4
- package/dist/init/client-init.errors.js +4 -4
- package/dist/init/client-init.orchestrator.js +37 -22
- package/dist/init/client-init.questions.d.ts +5 -5
- package/dist/init/client-init.questions.js +10 -10
- package/dist/init/client-project.inspector.d.ts +2 -2
- package/dist/init/client-project.inspector.js +3 -3
- package/dist/init/command.runner.d.ts +2 -0
- package/dist/init/command.runner.js +4 -1
- package/dist/output/output.writer.d.ts +5 -5
- package/dist/output/output.writer.js +12 -12
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
The development-time generator for Aventara typed remote clients. It fetches a deployed framework's `ClientContract`
|
|
4
4
|
from `<entrypoint>/_contract`, verifies it, and writes a standalone TypeScript client into your project. The generated
|
|
5
|
-
client is
|
|
5
|
+
client is what you ship: at runtime it imports nothing from this package or from `@aventara/core`.
|
|
6
6
|
|
|
7
|
-
Every argument and result type in the generated client is core's own derivation
|
|
8
|
-
declarations, copied into the tree and instantiated over the deployment's ClientContract
|
|
9
|
-
|
|
7
|
+
Every argument and result type in the generated client is core's own derivation: `@aventara/core`'s published
|
|
8
|
+
declarations, copied into the tree and instantiated over the deployment's ClientContract. The client's types therefore
|
|
9
|
+
match the server's.
|
|
10
10
|
|
|
11
11
|
## Setting up a frontend: `avclient init`
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npx @aventara/client@
|
|
14
|
+
npx @aventara/client@rc init # in your frontend project; pnpm: pnpm dlx @aventara/client@rc init
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
It asks
|
|
17
|
+
It asks four things and writes the setup. Each answer can also come from a flag, and `--yes` takes every default:
|
|
18
18
|
|
|
19
19
|
| Flag | Question | Default |
|
|
20
20
|
|---|---|---|
|
|
@@ -23,12 +23,16 @@ It asks — or takes from flags, or with `--yes` takes every default — four th
|
|
|
23
23
|
| `--generate-at <dir>` | Where the client goes | `./src/api` |
|
|
24
24
|
| `--package-manager <npm\|pnpm>` | | the lockfile's, else the launching one, else npm |
|
|
25
25
|
|
|
26
|
-
It writes `framework.client.ts` (or reuses the config the project already has), the variable into `.env` (only with a
|
|
27
|
-
generate"` into the scripts and `@aventara/client` as an
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
It writes `framework.client.ts` (or reuses the config the project already has), the variable into `.env` (only with a
|
|
27
|
+
variable), `"avclient:generate": "avclient generate"` into the scripts, and `@aventara/client` as an exact
|
|
28
|
+
devDependency at its own version. Then it runs the install (`--skip-install` prints it instead) and generates the
|
|
29
|
+
client right away (`--skip-generate` skips that). A server that does not answer leaves the files in place and names
|
|
30
|
+
`avclient generate`; `--verbose` shows the install's output as it runs.
|
|
31
|
+
|
|
32
|
+
Each step (the install, loading the config, fetching the ClientContract, emitting, checking and writing) is shown on
|
|
33
|
+
stderr while it runs: live on a terminal, and one plain line per step in a pipe, in CI or with `NO_COLOR`. Existing
|
|
34
|
+
content that differs is listed and replaced only when you confirm, or with `--yes`; where nobody can be asked, nothing
|
|
35
|
+
is touched. Commit the generated tree, because regenerating it needs a running server.
|
|
32
36
|
|
|
33
37
|
## Generating a client
|
|
34
38
|
|
|
@@ -50,19 +54,26 @@ and run:
|
|
|
50
54
|
npx avclient generate [--yes]
|
|
51
55
|
```
|
|
52
56
|
|
|
53
|
-
|
|
54
|
-
`.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
-
|
|
59
|
-
|
|
60
|
-
-
|
|
61
|
-
`AvClient.ts` and `generated
|
|
57
|
+
The config file is loaded the way Prisma 7 loads `prisma.config.ts`: `framework.client.ts` (or `.mts`, `.cts`, `.js`,
|
|
58
|
+
`.mjs`, `.cjs`), in ES module or CommonJS syntax, whatever your `package.json`'s `"type"`. A `framework.client.mts`
|
|
59
|
+
written by 0.1.0-pilot.1 keeps working, and `avclient init` reuses it. Two config files in one directory are refused,
|
|
60
|
+
naming both.
|
|
61
|
+
|
|
62
|
+
- `entrypoint` is the deployment's framework entrypoint: origin plus mount path, one absolute `http(s)` URL. It may
|
|
63
|
+
not carry credentials, because the platform's `fetch` refuses such a URL and the entrypoint ships inside the client.
|
|
64
|
+
- `generateAt` is a directory you may share with your own files. The generator owns exactly two entries in it,
|
|
65
|
+
`AvClient.ts` and `generated/`, and never reads, moves or removes anything else there.
|
|
66
|
+
- The `.env` cascade is read from the current directory before the config is evaluated, highest precedence first: the
|
|
67
|
+
process environment, `.env.<mode>.local`, `.env.<mode>`, `.env.local`, `.env`; `mode` is `NODE_ENV`, or
|
|
68
|
+
`development`. The config reads it through `env("NAME")`; `process.env` is never written.
|
|
69
|
+
- Content the generator did not produce in `AvClient.ts` or `generated/` is listed, and you are asked before it is
|
|
70
|
+
overwritten or removed. That includes what a killed run left in the `generated/` it moved aside. `--yes` answers yes.
|
|
71
|
+
Where nobody can answer (stdin is not a terminal, as in CI), the run is refused and nothing is touched. A cancelled
|
|
72
|
+
or refused run still prints every warning it raised.
|
|
62
73
|
|
|
63
74
|
### Your tsconfig decides how the client is written
|
|
64
75
|
|
|
65
|
-
The client is TypeScript source that
|
|
76
|
+
The client is TypeScript source that your own toolchain compiles, like your own files: `tsc`, Next.js (Turbopack or
|
|
66
77
|
webpack), Vite, `tsx`, Node's type stripping. Its modules import each other the way your project imports its files,
|
|
67
78
|
read from your `tsconfig.json` (the nearest one above `generateAt`, `extends` included) by Prisma 7's rules:
|
|
68
79
|
|
|
@@ -70,7 +81,7 @@ read from your `tsconfig.json` (the nearest one above `generateAt`, `extends` in
|
|
|
70
81
|
|---|---|---|
|
|
71
82
|
| `allowImportingTsExtensions` or `rewriteRelativeImportExtensions` (Vite's react-ts template, Node's type stripping) | `./generated/client.ts` | `./api/AvClient.ts` |
|
|
72
83
|
| `moduleResolution: "bundler"` (Next.js) or `module: "commonjs"` | `./generated/client` | `./api/AvClient` |
|
|
73
|
-
| anything else
|
|
84
|
+
| anything else, such as `module: "nodenext"`/`"node16"` (NestJS 12) | `./generated/client.js` | `./api/AvClient.js` (ES module) or `./api/AvClient` (CommonJS) |
|
|
74
85
|
|
|
75
86
|
Under `nodenext`, the nearest `package.json`'s `"type"` decides whether the client is checked as an ES module or as
|
|
76
87
|
CommonJS. Vite's `tsconfig.json`, which only lists `references`, defers to the referenced project that includes the
|
|
@@ -78,29 +89,25 @@ client (`tsconfig.app.json`). The validate step type-checks the client in your p
|
|
|
78
89
|
|
|
79
90
|
A project with no `tsconfig.json` is refused: the generated client is TypeScript, and JavaScript projects are not
|
|
80
91
|
supported yet.
|
|
81
|
-
- **The `.env` cascade** is read from the current directory before the config is evaluated, highest precedence first:
|
|
82
|
-
the process environment, `.env.<mode>.local`, `.env.<mode>`, `.env.local`, `.env`; `mode` is `NODE_ENV`, or
|
|
83
|
-
`development`. The config reads it through `env("NAME")`; `process.env` is never written.
|
|
84
|
-
- **Content the generator did not produce** in `AvClient.ts` or `generated/` is listed and you are asked before it is
|
|
85
|
-
overwritten or removed — including what a killed run left in the `generated/` it moved aside. `--yes` answers yes.
|
|
86
|
-
Where nobody can answer — stdin is not a terminal, as in CI — the run is refused and nothing is touched. A cancelled
|
|
87
|
-
or refused run still prints every warning it raised.
|
|
88
92
|
|
|
89
93
|
### What a run guarantees
|
|
90
94
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
95
|
+
A failed run leaves the previous output exactly as it was. The whole tree is written to a staging directory inside
|
|
96
|
+
`generateAt`, validated there as one program, and only then moved into place. A run killed part-way is repaired by the
|
|
97
|
+
next run that writes, before it writes anything. A first run that fails removes the directories it created.
|
|
98
|
+
|
|
99
|
+
The validate step is a real type check when your project has the optional peer `typescript` (5.5 to 6). It is resolved
|
|
100
|
+
from your project, so `npx @aventara/client@rc init` type-checks too, under a consumer's strictest plausible settings
|
|
101
|
+
and with nothing outside the tree resolvable. Without it, or under TypeScript 7 (which has no classic compiler API to
|
|
102
|
+
check with), the check falls back to a parse with Node's own TypeScript parser and says so in one warning. It is never
|
|
103
|
+
skipped, and the client is still written. The peer range has no upper bound, so a frontend on TypeScript 7 installs
|
|
104
|
+
it; your own `tsc` checks the tree when it compiles.
|
|
105
|
+
|
|
106
|
+
Two runs over one ClientContract write the same bytes, and deleting the client and regenerating it gives the same
|
|
107
|
+
bytes. Nothing volatile (no timestamp, generator version, host or path) is emitted.
|
|
108
|
+
|
|
109
|
+
A refusal is one line and exit code 1, with no stack; anything the run warned about before it stopped is printed
|
|
110
|
+
first. A stack means a defect in the generator.
|
|
104
111
|
|
|
105
112
|
## Calling the API
|
|
106
113
|
|
|
@@ -113,22 +120,22 @@ const one: User = await avClient.User.find.unique({ where: { id } });
|
|
|
113
120
|
const maybe = await avClient.User.find.first({ where }); // User | null — a miss is null
|
|
114
121
|
```
|
|
115
122
|
|
|
116
|
-
-
|
|
117
|
-
`new AvClient({ entrypoint, fetch })` makes another
|
|
118
|
-
ClientContract (tests, SSR), `fetch` for a custom fetch.
|
|
119
|
-
-
|
|
120
|
-
advertises exists, in the type and at runtime. A Resource named like one of the client's own members
|
|
121
|
-
`transaction`, `then`, `constructor`, `Object.prototype`'s names) is reached as `<name>Model`, with one
|
|
122
|
-
wire name is unchanged.
|
|
123
|
-
-
|
|
124
|
-
outcome throws
|
|
125
|
-
`
|
|
126
|
-
-
|
|
127
|
-
Arguments are encoded and results revived for you; a value that cannot be read throws `
|
|
128
|
-
-
|
|
123
|
+
- `avClient` is the default export: the client of the deployment the tree was generated from.
|
|
124
|
+
`new AvClient({ entrypoint, fetch })` makes another. `entrypoint` is for a deployment serving exactly the same
|
|
125
|
+
ClientContract (tests, SSR), and `fetch` is for a custom fetch.
|
|
126
|
+
- The client has one property per Resource, one per family under it, and one per advertised variant. Only what the
|
|
127
|
+
ClientContract advertises exists, in the type and at runtime. A Resource named like one of the client's own members
|
|
128
|
+
(`tx`, `transaction`, `then`, `constructor`, `Object.prototype`'s names) is reached as `<name>Model`, with one
|
|
129
|
+
warning; its wire name is unchanged.
|
|
130
|
+
- A call resolves to the data: the record, a list, a count. A first-style miss (`A1001`) is `null`. Every other
|
|
131
|
+
outcome throws the `AvError` subclass of its code (`AvNotFoundError` for a strict-unique miss,
|
|
132
|
+
`AvContractMismatchError` for a stale client, …), or `AvTransportError` when no framework answer arrived.
|
|
133
|
+
- Values are application values: `bigint`, `Date`, `Uint8Array`, the client's `AvDecimal`, JSON values as they are.
|
|
134
|
+
Arguments are encoded and results revived for you; a value that cannot be read throws `AvTransportError`.
|
|
135
|
+
- Per-call options (`AvCallOptions`) are never sent in the body: `signal` (an abort rejects with what `fetch` rejected
|
|
129
136
|
with), `headers` (the framework's own identity headers are refused), and `requestId`, sent as `Aventara-Request-Id`
|
|
130
137
|
and winning over that header in `headers`.
|
|
131
|
-
-
|
|
138
|
+
- The named types (`User`, `UserWhere`, `UserUniqueWhere`, `UserOrderBy`, `UserCreateData`, `UserUpdateData`,
|
|
132
139
|
`UserSelect`, `UserInclude`) are named exports, each only when the operation it reads is advertised. A name that would
|
|
133
140
|
clash walks the rename ladder (`User` beside a Resource `UserWhere` becomes `UserModel`), one warning each.
|
|
134
141
|
|
|
@@ -143,17 +150,37 @@ const profile = avClient.tx.Profile.create.one({ data: { bio: "…", userId: cre
|
|
|
143
150
|
const [user, row] = await avClient.transaction([created, profile]);
|
|
144
151
|
```
|
|
145
152
|
|
|
153
|
+
A `$ref` stands for one field of an earlier step's result. It may be written in a step's data at any depth (a field's
|
|
154
|
+
value, the key a relation's `$connect`, `$disconnect`, `$set`, `$connectOrCreate` or `$upsert` names, the `where` of a
|
|
155
|
+
nested `$update` or `$delete`, and the data `$create`, `$update` and `$upsert` nest), in its `cursor`, and anywhere in
|
|
156
|
+
its `where`, `AND`/`OR`/`NOT` and relation filters included. A projection's nested `where` and `orderBy` take none:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const post = avClient.tx.Post.create.one({ data: { title: "Hi", author: { $connect: { id: created.$ref("id") } } } });
|
|
160
|
+
const mine = avClient.tx.Post.find.many({ where: { OR: [{ authorId: created.$ref("id") }, { title: { startsWith: created.$ref("email") } }] } });
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Its source field must have the same type as where it is written:
|
|
164
|
+
|
|
165
|
+
- for a filter's value, `equals` and `not`, a nullable source only where the field is nullable, and a `null` then
|
|
166
|
+
matches the rows where the field is unset, as a literal `null` does;
|
|
167
|
+
- for a unique key, a `cursor`, a comparison or a text operator, a non-null source;
|
|
168
|
+
- for `in`, `notIn`, `hasEvery` and `hasSome`, a list field;
|
|
169
|
+
- for `has`, one value.
|
|
170
|
+
|
|
171
|
+
A mismatch is a type error, and the server refuses it before anything runs (`A2007`/`V1011`).
|
|
172
|
+
|
|
146
173
|
`avClient.tx.…` builds a handle and sends nothing. `avClient.transaction([...])` sends the plan once and resolves one
|
|
147
174
|
result per handle, typed, in order; a failing step throws its error with `cause.operation` naming the step. `$ref` binds
|
|
148
175
|
to the handle's position in the list it runs with. Two mistakes are refused before anything is sent, as core refuses
|
|
149
|
-
them in process: one handle twice (`
|
|
150
|
-
(`
|
|
176
|
+
them in process: one handle twice (`AvValidationError`, `A2004`/`V1001`), and a `$ref` to a handle not in the list
|
|
177
|
+
(`AvValidationError`, `A2007`/`V1010`). A handle is plain data: any client of the same generated tree may run it.
|
|
151
178
|
|
|
152
179
|
## Custom fetch and authentication
|
|
153
180
|
|
|
154
|
-
`fetch` is typed as
|
|
155
|
-
`fetch` is accepted without a cast; with neither, a structural fetch type stands in. The client
|
|
156
|
-
own beyond the protocol's
|
|
181
|
+
`fetch` is typed as your platform's own `fetch` when your `lib` declares one (DOM, `@types/node`), so the platform
|
|
182
|
+
`fetch` is accepted without a cast; with neither, a structural fetch type stands in. The client adds no headers of its
|
|
183
|
+
own beyond the protocol's, so authentication goes in a custom fetch:
|
|
157
184
|
|
|
158
185
|
```ts
|
|
159
186
|
const client = new AvClient({
|
|
@@ -161,107 +188,116 @@ const client = new AvClient({
|
|
|
161
188
|
});
|
|
162
189
|
```
|
|
163
190
|
|
|
164
|
-
The platform `fetch` is read when a call is made, not when the client is imported
|
|
165
|
-
where there is
|
|
191
|
+
The platform `fetch` is read when a call is made, not when the client is imported. Importing the client never fails
|
|
192
|
+
where there is no `fetch`; a call does, saying why. An entrypoint carrying credentials is refused when the config is
|
|
166
193
|
resolved.
|
|
167
194
|
|
|
168
195
|
## Up to date
|
|
169
196
|
|
|
170
|
-
A rerun asks the deployment conditionally (`If-None-Match`, the ClientContract hash of the output it
|
|
171
|
-
|
|
172
|
-
|
|
197
|
+
A rerun asks the deployment conditionally (`If-None-Match`, the ClientContract hash of the output it finds), but only
|
|
198
|
+
when that output is intact and its carrier re-verifies. On `304 Not Modified` it re-emits from that stored
|
|
199
|
+
ClientContract with this generator and this entrypoint. Identical bytes print
|
|
173
200
|
`avclient: up to date: … nothing was written.` and exit 0; otherwise the output is replaced, and the line says
|
|
174
201
|
`the ClientContract is unchanged; the generator or the entrypoint changed`.
|
|
175
202
|
|
|
176
203
|
## What is emitted
|
|
177
204
|
|
|
178
205
|
Every file opens with a banner stating that it is generated and must not be edited, and with the Biome suppressions a
|
|
179
|
-
generated file needs
|
|
180
|
-
|
|
181
|
-
`generated/types.d.ts`; the `.ts` sources carry their own types.
|
|
182
|
-
|
|
183
|
-
- `AvClient.ts
|
|
184
|
-
`
|
|
185
|
-
subclasses, `
|
|
186
|
-
- `generated/client.ts
|
|
187
|
-
instance `avClient
|
|
188
|
-
- `generated/contract.ts
|
|
206
|
+
generated file needs. They are emitted rather than configured, so the output stays correct whatever formatter the
|
|
207
|
+
repository uses. The `.d.ts` files are core's copied declarations under `generated/derivation/`
|
|
208
|
+
and the named types in `generated/types.d.ts`; the `.ts` sources carry their own types.
|
|
209
|
+
|
|
210
|
+
- `AvClient.ts`: the entry point you import: `avClient` (the default export), `AvClient`, `AvClientOptions`,
|
|
211
|
+
`AvCallOptions`, `AvFetch`, `AvOperation`, the named Resource types, the enums, `AvDecimal`, `AvError` and its
|
|
212
|
+
subclasses, `AvTransportError`, and the types `AvCause`, `AvValidationIssue`, `AvOperationCode` and `AvValidationCode`.
|
|
213
|
+
- `generated/client.ts`: the typed client: the class `AvClient`, `AvClientOptions`, `AvCallOptions` and the ready
|
|
214
|
+
instance `avClient`. Every argument and result type is an instantiation of core's own derivation.
|
|
215
|
+
- `generated/contract.ts`: the ClientContract the client was generated against, its served canonical bytes verbatim,
|
|
189
216
|
as the type `ClientContractShape` (no runtime byte).
|
|
190
|
-
- `generated/derivation
|
|
191
|
-
|
|
192
|
-
- `generated/enums.ts
|
|
193
|
-
- `generated/metadata.ts
|
|
194
|
-
was generated from, its default deployment (an entrypoint carrying credentials is refused at resolution).
|
|
195
|
-
- `generated/runtime/codec.ts
|
|
196
|
-
- `generated/runtime/decimal.ts
|
|
197
|
-
- `generated/runtime/descriptor.ts
|
|
217
|
+
- `generated/derivation/`: `@aventara/core`'s published declarations of the argument/result derivation and the call
|
|
218
|
+
grammar, copied byte for byte (bannered, source-map trailer dropped), so the generated types are core's own.
|
|
219
|
+
- `generated/enums.ts`: each enum as a union type and a same-named `as const` object.
|
|
220
|
+
- `generated/metadata.ts`: the ClientContract hash and protocol version the client is bound to, and the entrypoint it
|
|
221
|
+
was generated from, which is its default deployment (an entrypoint carrying credentials is refused at resolution).
|
|
222
|
+
- `generated/runtime/codec.ts`: the nine scalars' wire codecs, and the request-body serializer.
|
|
223
|
+
- `generated/runtime/decimal.ts`: the framework's `AvDecimal`, with a constructor, `toString` and `toJSON`.
|
|
224
|
+
- `generated/runtime/descriptor.ts`: what the runtime reads of the contract: the result fields it revives and the
|
|
198
225
|
relations it follows (the decode table), and the advertised operations.
|
|
199
|
-
- `generated/runtime/errors.ts
|
|
200
|
-
`
|
|
201
|
-
the code unions, emitted from core's code arrays
|
|
202
|
-
- `generated/runtime/
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
- `generated/
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
226
|
+
- `generated/runtime/errors.ts`: `AvError`, its subclasses by code class (`AvAuthError`, `AvConflictError`,
|
|
227
|
+
`AvContractMismatchError`, `AvInternalError`, `AvNotFoundError`, `AvProtocolError`, `AvValidationError`),
|
|
228
|
+
`AvTransportError`, and the code unions, emitted from core's code arrays instead of retyped.
|
|
229
|
+
- `generated/runtime/field-check.evaluator.ts`: core's own field-check evaluator, copied byte for byte with its
|
|
230
|
+
imports pointed at `generated/derivation/`. Emitted only when the ClientContract carries a check (a field's
|
|
231
|
+
`validators`, or a check on a Json Shape node).
|
|
232
|
+
- `generated/runtime/field-validators.ts`: the ClientContract's checks as data, and the walk that runs them on an
|
|
233
|
+
operation's arguments before anything is sent. A failure throws the server's `A2004` with the same issues. A check
|
|
234
|
+
the client cannot judge exactly as the server would is left to the server. Emitted only when the ClientContract
|
|
235
|
+
carries a check.
|
|
236
|
+
- `generated/runtime/fingerprint.ts`: the operation fingerprint (RFC 8785 canonical JSON, SHA-256, base64url),
|
|
237
|
+
dependency-free and pinned to core's. Emitted only when transactions are `interactive`.
|
|
238
|
+
- `generated/runtime/transaction.ts`: `avClient.tx`'s deferred handles and `avClient.transaction`'s plan assembly,
|
|
239
|
+
refusals and one POST to `/_transactions`. Emitted only when transactions are `interactive`.
|
|
240
|
+
- `generated/runtime/transport.ts`: one POST per operation with the identity headers, and the response read by the
|
|
241
|
+
protocol's rule. Arguments are encoded by value, results revived by the decode table, and the platform's own `fetch`
|
|
242
|
+
type is accepted. Its `execute` primitive is module-private: no public file re-exports it, and `AvClient`'s methods
|
|
243
|
+
wrap it.
|
|
244
|
+
- `generated/types.d.ts`: the named Resource types, as a declaration file (`User`, `UserWhere`, `UserUniqueWhere`,
|
|
245
|
+
`UserOrderBy`, `UserCreateData`, `UserUpdateData`, `UserSelect`, `UserInclude`), each only when its operation is
|
|
246
|
+
advertised.
|
|
247
|
+
|
|
248
|
+
The tree is self-contained: every import in it names a file in it, so it compiles with nothing else installed. Names a
|
|
249
|
+
TypeScript declaration cannot carry as-is (a reserved word, a name the runtime owns, an enum sharing a Resource's name)
|
|
250
|
+
are renamed, with one warning each; the wire name is kept.
|
|
216
251
|
|
|
217
252
|
## What a generated client is bound to
|
|
218
253
|
|
|
219
|
-
A generated client is bound to the
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
`
|
|
254
|
+
A generated client is bound to the exact ClientContract hash and protocol version it was generated from. Both are in
|
|
255
|
+
`generated/metadata.ts`, and both are sent on every request (`Aventara-Protocol-Version`, `Aventara-Contract-Hash`). A
|
|
256
|
+
deployment whose ClientContract differs refuses the request with `A2005`, which the client throws as
|
|
257
|
+
`AvContractMismatchError`.
|
|
223
258
|
|
|
224
|
-
The hash covers everything the deployment advertises, not only its schema
|
|
259
|
+
The hash covers everything the deployment advertises, not only its schema. Two deployments of one schema can advertise
|
|
225
260
|
different ClientContracts, and a client generated against one is refused by the other. Whatever the cause, the remedy
|
|
226
|
-
is the same
|
|
261
|
+
is the same: generate against the deployment the client will call, and regenerate when it changes.
|
|
227
262
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
263
|
+
The same holds for an operation the deployment no longer advertises. The server checks the client's identity before it
|
|
264
|
+
routes, so a stale client calling a removed operation also hears `A2005` and throws `AvContractMismatchError`. A
|
|
265
|
+
response that is not a framework envelope at all, such as a host's own 404 for a path it does not mount, is an
|
|
266
|
+
`AvTransportError`. It names no remedy, because nothing in it says the client is stale.
|
|
232
267
|
|
|
233
|
-
The embedded entrypoint is a
|
|
234
|
-
|
|
268
|
+
The embedded entrypoint is a default. `new AvClient({ entrypoint })` may point a client at another deployment without
|
|
269
|
+
re-binding anything: the ClientContract hash still decides whether that deployment accepts the client.
|
|
235
270
|
|
|
236
271
|
## Type-checking cost in a consumer
|
|
237
272
|
|
|
238
|
-
|
|
239
|
-
|
|
273
|
+
Under `skipLibCheck: true` (the common default), a consumer pays for the calls it type-checks, not for the schema: the
|
|
274
|
+
named types (`generated/types.d.ts`) and core's derivation are declaration files checked only where read. Under
|
|
240
275
|
`skipLibCheck: false` every named type is resolved where it is declared, so the cost grows with the number of
|
|
241
276
|
Resources.
|
|
242
277
|
|
|
243
278
|
## Before 1.0
|
|
244
279
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
280
|
+
The `Aventara` wire prefix of the protocol's headers (`Aventara-Protocol-Version`, `Aventara-Contract-Hash`,
|
|
281
|
+
`Aventara-Request-Id`) may still change before the first stable release. It is one constant in the generated
|
|
282
|
+
`generated/runtime/transport.ts`, and regenerating picks up a change.
|
|
248
283
|
|
|
249
284
|
## Known issues
|
|
250
285
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
286
|
+
The generator can read the wrong `tsconfig.json`. In a monorepo or a non-standard layout, the nearest `tsconfig.json`
|
|
287
|
+
above `generateAt` may not be the one your project compiles the client with. The client's imports or module format then
|
|
288
|
+
do not match your compiler (`Cannot find module './generated/client.js'`, or an extension your bundler will not
|
|
289
|
+
resolve). Name the right one in `framework.client.ts`, relative to the config file:
|
|
255
290
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
291
|
+
```ts
|
|
292
|
+
export default defineClientConfig({
|
|
293
|
+
entrypoint: env("AVENTARA_API_URL"),
|
|
294
|
+
generateAt: "./src/api",
|
|
295
|
+
tsconfigFile: "./tsconfig.app.json",
|
|
296
|
+
});
|
|
297
|
+
```
|
|
263
298
|
|
|
264
299
|
## License
|
|
265
300
|
|
|
266
|
-
PolyForm Shield 1.0.0 with an additional permission
|
|
267
|
-
use it to build a product that competes with Aventara itself. See `LICENSE` and
|
|
301
|
+
PolyForm Shield 1.0.0 with an additional permission. It is free to use, including in commercial applications, but you
|
|
302
|
+
may not use it to build a product that competes with Aventara itself. See `LICENSE` and
|
|
303
|
+
`LICENSE-ADDITIONAL-PERMISSION.md`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare const USAGE = "avclient \u2014 generate a typed Aventara client from a deployed ClientContract.\n\nUsage:\n avclient generate [--yes] Read framework.client.ts (or .js, .mjs, .cjs, .mts,\n .cts) in the current directory, fetch\n <entrypoint>/_contract, and write AvClient.ts and\n generated/ into its generateAt directory, its imports\n spelled as the project's tsconfig.json says.\n avclient init [options] Set up this frontend: write framework.client.ts, the\n .env entry, the avclient:generate script and the\n @aventara/client devDependency; install; generate.\n --entrypoint <url> The server's entrypoint [http://localhost:3000/api].\n --env-var <NAME> Read it from this variable [AVENTARA_API_URL].\n --no-env-var Write it into framework.client.ts as a literal.\n --generate-at <dir> Where the client goes [./src/api].\n --package-manager <npm|pnpm> [the lockfile's, else the launching one, else npm]\n --skip-install Print the install instead of running it.\n --skip-generate Do not generate now.\n -y, --yes Accept every default, and replace differing content.\n avclient --help Print this and exit 0.\n avclient <command> --help Print that command's usage and exit 0.\n avclient --version, -v Print this generator's version and exit 0.\n\nOptions:\n -y, --yes Overwrite or remove content in AvClient.ts and generated/ that the\n generator did not produce, without asking. Without it, such content\n is listed and you are asked; where nobody can answer (stdin is not a\n terminal, as in CI), the run is refused and nothing is touched.\n\nThe generator owns AvClient.ts and generated/ in generateAt, and nothing else\nthere: your own files beside them are never read, moved or removed. Generated\nfiles are replaced on every run; do not edit them.\n\nBefore framework.client.ts is evaluated, the .env cascade is read from the\ncurrent directory, highest precedence first: the process environment,\n.env.<mode>.local, .env.<mode>, .env.local, .env \u2014 where mode is NODE_ENV, or\n\"development\".\n";
|
|
2
2
|
export declare const GENERATE_USAGE = "Usage: avclient generate [--yes]\n\nRead framework.client.ts (or .js, .mjs, .cjs, .mts, .cts) in the current directory,\nfetch <entrypoint>/_contract, and write AvClient.ts and generated/ into its\ngenerateAt directory, its imports spelled as the project's tsconfig.json says.\n\nOptions:\n -y, --yes Overwrite or remove content in AvClient.ts and generated/ that the\n generator did not produce, without asking. Without it, such content\n is listed and you are asked; where nobody can answer (stdin is not a\n terminal, as in CI), the run is refused and nothing is touched.\n\nThe generator owns AvClient.ts and generated/ in generateAt, and nothing else\nthere: your own files beside them are never read, moved or removed.\n\nBefore the config is evaluated, the .env cascade is read from the current\ndirectory, highest precedence first: the process environment, .env.<mode>.local,\n.env.<mode>, .env.local, .env \u2014 where mode is NODE_ENV, or \"development\".\n";
|
|
3
|
-
export declare const INIT_USAGE = "Usage: avclient init [options]\n\nSet up this frontend: write framework.client.ts, the .env entry, the avclient:generate\nscript and the @aventara/client devDependency; install; generate.\n\nOptions:\n --entrypoint <url> The server's entrypoint [http://localhost:3000/api].\n --env-var <NAME> Read it from this variable [AVENTARA_API_URL].\n --no-env-var Write it into framework.client.ts as a literal.\n --generate-at <dir> Where the client goes [./src/api].\n --package-manager <npm|pnpm> [the lockfile's, else the launching one, else npm]\n --skip-install Print the install instead of running it.\n --skip-generate Do not generate now.\n -y, --yes Accept every default, and replace differing content.\n\nOn a terminal every unanswered question is asked; anywhere else, pass its flag\nor --yes, or the run stops before writing anything.\n";
|
|
3
|
+
export declare const INIT_USAGE = "Usage: avclient init [options]\n\nSet up this frontend: write framework.client.ts, the .env entry, the avclient:generate\nscript and the @aventara/client devDependency; install; generate.\n\nOptions:\n --entrypoint <url> The server's entrypoint [http://localhost:3000/api].\n --env-var <NAME> Read it from this variable [AVENTARA_API_URL].\n --no-env-var Write it into framework.client.ts as a literal.\n --generate-at <dir> Where the client goes [./src/api].\n --package-manager <npm|pnpm> [the lockfile's, else the launching one, else npm]\n --skip-install Print the install instead of running it.\n --skip-generate Do not generate now.\n --verbose Show the install's output as it runs.\n -y, --yes Accept every default, and replace differing content.\n\nOn a terminal every unanswered question is asked; anywhere else, pass its flag\nor --yes, or the run stops before writing anything.\n";
|
|
4
4
|
export type ClientInitCommand = {
|
|
5
5
|
readonly command: "init";
|
|
6
6
|
readonly given: Readonly<Partial<Record<"entrypoint" | "envVar" | "generateAt" | "packageManager", string>>>;
|
|
@@ -8,6 +8,8 @@ export type ClientInitCommand = {
|
|
|
8
8
|
readonly skipInstall: boolean;
|
|
9
9
|
readonly skipGenerate: boolean;
|
|
10
10
|
readonly yes: boolean;
|
|
11
|
+
/** Streams the install's output. */
|
|
12
|
+
readonly verbose?: boolean;
|
|
11
13
|
};
|
|
12
14
|
/** What the command line asked for. */
|
|
13
15
|
export type CliCommand = {
|
|
@@ -20,11 +22,11 @@ export type CliCommand = {
|
|
|
20
22
|
readonly yes: boolean;
|
|
21
23
|
} | ClientInitCommand;
|
|
22
24
|
/** The command line cannot be understood. A refusal: one sentence, exit 1. */
|
|
23
|
-
export declare class
|
|
24
|
-
readonly name = "
|
|
25
|
+
export declare class AvCliCommandError extends Error {
|
|
26
|
+
readonly name = "AvCliCommandError";
|
|
25
27
|
}
|
|
26
28
|
/**
|
|
27
|
-
* @throws
|
|
29
|
+
* @throws AvCliCommandError when `argv` is not `generate [--yes|-y]`, `init …`,
|
|
28
30
|
* `--help`/`-h` or `--version`/`-v`.
|
|
29
31
|
*/
|
|
30
32
|
export declare function parseCliCommand(argv: readonly string[]): CliCommand;
|
|
@@ -69,19 +69,20 @@ Options:
|
|
|
69
69
|
--package-manager <npm|pnpm> [the lockfile's, else the launching one, else npm]
|
|
70
70
|
--skip-install Print the install instead of running it.
|
|
71
71
|
--skip-generate Do not generate now.
|
|
72
|
+
--verbose Show the install's output as it runs.
|
|
72
73
|
-y, --yes Accept every default, and replace differing content.
|
|
73
74
|
|
|
74
75
|
On a terminal every unanswered question is asked; anywhere else, pass its flag
|
|
75
76
|
or --yes, or the run stops before writing anything.
|
|
76
77
|
`;
|
|
77
|
-
export class
|
|
78
|
-
name = "
|
|
78
|
+
export class AvCliCommandError extends Error {
|
|
79
|
+
name = "AvCliCommandError";
|
|
79
80
|
}
|
|
80
81
|
export function parseCliCommand(argv) {
|
|
81
82
|
const [command, ...rest] = argv;
|
|
82
83
|
const help = "run `avclient --help` for usage.";
|
|
83
84
|
if (command === undefined) {
|
|
84
|
-
throw new
|
|
85
|
+
throw new AvCliCommandError(`a command is required; ${help}`);
|
|
85
86
|
}
|
|
86
87
|
if (command === "--help" || command === "-h") {
|
|
87
88
|
return { command: "help" };
|
|
@@ -97,7 +98,7 @@ export function parseCliCommand(argv) {
|
|
|
97
98
|
return parseInit(rest, help);
|
|
98
99
|
}
|
|
99
100
|
if (command !== "generate") {
|
|
100
|
-
throw new
|
|
101
|
+
throw new AvCliCommandError(`unknown command ${JSON.stringify(command)}; ${help}`);
|
|
101
102
|
}
|
|
102
103
|
let yes = false;
|
|
103
104
|
for (const argument of rest) {
|
|
@@ -105,7 +106,7 @@ export function parseCliCommand(argv) {
|
|
|
105
106
|
yes = true;
|
|
106
107
|
continue;
|
|
107
108
|
}
|
|
108
|
-
throw new
|
|
109
|
+
throw new AvCliCommandError(`unexpected argument ${JSON.stringify(argument)}: generate takes only --yes, ` +
|
|
109
110
|
`everything else it needs is in ${CLIENT_CONFIG_FILE}; ${help}`);
|
|
110
111
|
}
|
|
111
112
|
return { command: "generate", yes };
|
|
@@ -132,24 +133,29 @@ function parseInit(argv, help) {
|
|
|
132
133
|
yes: false,
|
|
133
134
|
};
|
|
134
135
|
const seen = new Set();
|
|
136
|
+
let verbose = false;
|
|
135
137
|
for (let index = 0; index < argv.length; index += 1) {
|
|
136
138
|
const argument = argv[index];
|
|
139
|
+
if (argument === "--verbose" && !verbose) {
|
|
140
|
+
verbose = true;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
137
143
|
const equals = argument.indexOf("=");
|
|
138
144
|
const flag = equals === -1 ? argument : argument.slice(0, equals);
|
|
139
145
|
const inline = equals === -1 ? undefined : argument.slice(equals + 1);
|
|
140
146
|
const value = INIT_VALUES[flag];
|
|
141
147
|
const toggle = INIT_SWITCHES[flag];
|
|
142
148
|
if (value === undefined && toggle === undefined) {
|
|
143
|
-
throw new
|
|
149
|
+
throw new AvCliCommandError(`unexpected argument ${JSON.stringify(argument)} for init; ${help}`);
|
|
144
150
|
}
|
|
145
151
|
const canonical = toggle === "yes" ? "--yes" : flag;
|
|
146
152
|
if (seen.has(canonical)) {
|
|
147
|
-
throw new
|
|
153
|
+
throw new AvCliCommandError(`${canonical} was given twice; ${help}`);
|
|
148
154
|
}
|
|
149
155
|
seen.add(canonical);
|
|
150
156
|
if (toggle !== undefined) {
|
|
151
157
|
if (inline !== undefined) {
|
|
152
|
-
throw new
|
|
158
|
+
throw new AvCliCommandError(`${canonical} takes no value; ${help}`);
|
|
153
159
|
}
|
|
154
160
|
switches[toggle] = true;
|
|
155
161
|
continue;
|
|
@@ -157,7 +163,7 @@ function parseInit(argv, help) {
|
|
|
157
163
|
const taken = inline ?? argv[index + 1];
|
|
158
164
|
if (taken === undefined ||
|
|
159
165
|
(inline === undefined && taken.startsWith("-"))) {
|
|
160
|
-
throw new
|
|
166
|
+
throw new AvCliCommandError(`${canonical} takes a value; ${help}`);
|
|
161
167
|
}
|
|
162
168
|
if (inline === undefined) {
|
|
163
169
|
index += 1;
|
|
@@ -165,7 +171,12 @@ function parseInit(argv, help) {
|
|
|
165
171
|
given[value] = taken;
|
|
166
172
|
}
|
|
167
173
|
if (switches.noEnvVar && given.envVar !== undefined) {
|
|
168
|
-
throw new
|
|
174
|
+
throw new AvCliCommandError(`--env-var and --no-env-var contradict each other; ${help}`);
|
|
169
175
|
}
|
|
170
|
-
return {
|
|
176
|
+
return {
|
|
177
|
+
command: "init",
|
|
178
|
+
given,
|
|
179
|
+
...switches,
|
|
180
|
+
...(verbose ? { verbose: true } : {}),
|
|
181
|
+
};
|
|
171
182
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ProcessEnvInput } from "../config/env.cascade.js";
|
|
2
2
|
import type { CommandRunner } from "../init/command.runner.js";
|
|
3
|
+
import { type ProgressMode, type StepReporter } from "./step.reporter.js";
|
|
3
4
|
/** What `runCli` reads and writes instead of the process globals. */
|
|
4
5
|
export interface CliIo {
|
|
5
6
|
readonly cwd: string;
|
|
@@ -14,11 +15,15 @@ export interface CliIo {
|
|
|
14
15
|
readonly ask?: (prompt: string) => Promise<string>;
|
|
15
16
|
/** Runs a command with stdout piped (`init`'s install); a child process by default. */
|
|
16
17
|
readonly run?: CommandRunner;
|
|
18
|
+
/** How the steps are shown on stderr; none when omitted. */
|
|
19
|
+
readonly progress?: ProgressMode;
|
|
17
20
|
}
|
|
21
|
+
/** How many steps a generation shows: the config, the contract, the emission, the write. */
|
|
22
|
+
export declare const GENERATION_STEPS = 4;
|
|
18
23
|
/**
|
|
19
24
|
* `avclient generate` — the generation, the generateAt rule's question, and the
|
|
20
25
|
* report. Shared by `init`, which generates once the project is set up.
|
|
21
26
|
*
|
|
22
27
|
* @throws whatever the generation raises; the caller renders it.
|
|
23
28
|
*/
|
|
24
|
-
export declare function runGenerate(io: CliIo, yes: boolean): Promise<number>;
|
|
29
|
+
export declare function runGenerate(io: CliIo, yes: boolean, reporter?: StepReporter): Promise<number>;
|