@aventara/client 1.0.0-rc.0 → 1.0.0-rc.2
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 +504 -0
- package/README.md +159 -141
- package/dist/cli/command.parser.d.ts +5 -5
- package/dist/cli/command.parser.js +13 -13
- package/dist/cli/generation-failure.renderer.js +24 -24
- package/dist/config/client-config.interface.d.ts +7 -7
- package/dist/config/config.loader.d.ts +10 -4
- package/dist/config/config.loader.js +16 -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 +61 -40
- 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/derivation.emitter.js +1 -0
- package/dist/emit/descriptor.emitter.d.ts +2 -2
- package/dist/emit/descriptor.emitter.js +1 -1
- 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 +592 -0
- package/dist/emit/json-shape.emitter.d.ts +2 -2
- package/dist/emit/name.deriver.d.ts +6 -6
- package/dist/emit/name.deriver.js +14 -14
- 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 +166 -112
- package/dist/emit/scalar.codec.d.ts +1 -1
- package/dist/emit/scalar.codec.js +246 -29
- package/dist/emit/transaction.emitter.js +30 -14
- package/dist/generate.d.ts +2 -2
- package/dist/generate.js +2 -2
- 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 +7 -7
- 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/output/output.validator.d.ts +3 -4
- package/dist/output/output.validator.js +2 -2
- package/dist/output/output.writer.d.ts +5 -5
- package/dist/output/output.writer.js +12 -12
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
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
|
|
|
@@ -14,7 +14,7 @@ server by construction.
|
|
|
14
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,14 +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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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.
|
|
34
36
|
|
|
35
37
|
## Generating a client
|
|
36
38
|
|
|
@@ -52,19 +54,26 @@ and run:
|
|
|
52
54
|
npx avclient generate [--yes]
|
|
53
55
|
```
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
`.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
-
|
|
61
|
-
|
|
62
|
-
-
|
|
63
|
-
`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.
|
|
64
73
|
|
|
65
74
|
### Your tsconfig decides how the client is written
|
|
66
75
|
|
|
67
|
-
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
|
|
68
77
|
webpack), Vite, `tsx`, Node's type stripping. Its modules import each other the way your project imports its files,
|
|
69
78
|
read from your `tsconfig.json` (the nearest one above `generateAt`, `extends` included) by Prisma 7's rules:
|
|
70
79
|
|
|
@@ -72,7 +81,7 @@ read from your `tsconfig.json` (the nearest one above `generateAt`, `extends` in
|
|
|
72
81
|
|---|---|---|
|
|
73
82
|
| `allowImportingTsExtensions` or `rewriteRelativeImportExtensions` (Vite's react-ts template, Node's type stripping) | `./generated/client.ts` | `./api/AvClient.ts` |
|
|
74
83
|
| `moduleResolution: "bundler"` (Next.js) or `module: "commonjs"` | `./generated/client` | `./api/AvClient` |
|
|
75
|
-
| anything else
|
|
84
|
+
| anything else, such as `module: "nodenext"`/`"node16"` (NestJS 12) | `./generated/client.js` | `./api/AvClient.js` (ES module) or `./api/AvClient` (CommonJS) |
|
|
76
85
|
|
|
77
86
|
Under `nodenext`, the nearest `package.json`'s `"type"` decides whether the client is checked as an ES module or as
|
|
78
87
|
CommonJS. Vite's `tsconfig.json`, which only lists `references`, defers to the referenced project that includes the
|
|
@@ -80,29 +89,25 @@ client (`tsconfig.app.json`). The validate step type-checks the client in your p
|
|
|
80
89
|
|
|
81
90
|
A project with no `tsconfig.json` is refused: the generated client is TypeScript, and JavaScript projects are not
|
|
82
91
|
supported yet.
|
|
83
|
-
- **The `.env` cascade** is read from the current directory before the config is evaluated, highest precedence first:
|
|
84
|
-
the process environment, `.env.<mode>.local`, `.env.<mode>`, `.env.local`, `.env`; `mode` is `NODE_ENV`, or
|
|
85
|
-
`development`. The config reads it through `env("NAME")`; `process.env` is never written.
|
|
86
|
-
- **Content the generator did not produce** in `AvClient.ts` or `generated/` is listed and you are asked before it is
|
|
87
|
-
overwritten or removed — including what a killed run left in the `generated/` it moved aside. `--yes` answers yes.
|
|
88
|
-
Where nobody can answer — stdin is not a terminal, as in CI — the run is refused and nothing is touched. A cancelled
|
|
89
|
-
or refused run still prints every warning it raised.
|
|
90
92
|
|
|
91
93
|
### What a run guarantees
|
|
92
94
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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.7 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.
|
|
106
111
|
|
|
107
112
|
## Calling the API
|
|
108
113
|
|
|
@@ -115,22 +120,22 @@ const one: User = await avClient.User.find.unique({ where: { id } });
|
|
|
115
120
|
const maybe = await avClient.User.find.first({ where }); // User | null — a miss is null
|
|
116
121
|
```
|
|
117
122
|
|
|
118
|
-
-
|
|
119
|
-
`new AvClient({ entrypoint, fetch })` makes another
|
|
120
|
-
ClientContract (tests, SSR), `fetch` for a custom fetch.
|
|
121
|
-
-
|
|
122
|
-
advertises exists, in the type and at runtime. A Resource named like one of the client's own members
|
|
123
|
-
`transaction`, `then`, `constructor`, `Object.prototype`'s names) is reached as `<name>Model`, with one
|
|
124
|
-
wire name is unchanged.
|
|
125
|
-
-
|
|
126
|
-
outcome throws
|
|
127
|
-
`
|
|
128
|
-
-
|
|
129
|
-
Arguments are encoded and results revived for you; a value that cannot be read throws `
|
|
130
|
-
-
|
|
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
|
|
131
136
|
with), `headers` (the framework's own identity headers are refused), and `requestId`, sent as `Aventara-Request-Id`
|
|
132
137
|
and winning over that header in `headers`.
|
|
133
|
-
-
|
|
138
|
+
- The named types (`User`, `UserWhere`, `UserUniqueWhere`, `UserOrderBy`, `UserCreateData`, `UserUpdateData`,
|
|
134
139
|
`UserSelect`, `UserInclude`) are named exports, each only when the operation it reads is advertised. A name that would
|
|
135
140
|
clash walks the rename ladder (`User` beside a Resource `UserWhere` becomes `UserModel`), one warning each.
|
|
136
141
|
|
|
@@ -145,33 +150,37 @@ const profile = avClient.tx.Profile.create.one({ data: { bio: "…", userId: cre
|
|
|
145
150
|
const [user, row] = await avClient.transaction([created, profile]);
|
|
146
151
|
```
|
|
147
152
|
|
|
148
|
-
A `$ref` stands for one field of an earlier step's result
|
|
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
|
|
149
154
|
value, the key a relation's `$connect`, `$disconnect`, `$set`, `$connectOrCreate` or `$upsert` names, the `where` of a
|
|
150
|
-
nested `$update` or `$delete`, and the data `$create`, `$update` and `$upsert` nest), its `cursor`, and anywhere in
|
|
151
|
-
`where
|
|
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:
|
|
152
157
|
|
|
153
158
|
```ts
|
|
154
159
|
const post = avClient.tx.Post.create.one({ data: { title: "Hi", author: { $connect: { id: created.$ref("id") } } } });
|
|
155
160
|
const mine = avClient.tx.Post.find.many({ where: { OR: [{ authorId: created.$ref("id") }, { title: { startsWith: created.$ref("email") } }] } });
|
|
156
161
|
```
|
|
157
162
|
|
|
158
|
-
Its source field must have the same type as where it is written:
|
|
159
|
-
|
|
160
|
-
|
|
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`).
|
|
163
172
|
|
|
164
173
|
`avClient.tx.…` builds a handle and sends nothing. `avClient.transaction([...])` sends the plan once and resolves one
|
|
165
174
|
result per handle, typed, in order; a failing step throws its error with `cause.operation` naming the step. `$ref` binds
|
|
166
175
|
to the handle's position in the list it runs with. Two mistakes are refused before anything is sent, as core refuses
|
|
167
|
-
them in process: one handle twice (`
|
|
168
|
-
(`
|
|
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.
|
|
169
178
|
|
|
170
179
|
## Custom fetch and authentication
|
|
171
180
|
|
|
172
|
-
`fetch` is typed as
|
|
173
|
-
`fetch` is accepted without a cast; with neither, a structural fetch type stands in. The client
|
|
174
|
-
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:
|
|
175
184
|
|
|
176
185
|
```ts
|
|
177
186
|
const client = new AvClient({
|
|
@@ -179,107 +188,116 @@ const client = new AvClient({
|
|
|
179
188
|
});
|
|
180
189
|
```
|
|
181
190
|
|
|
182
|
-
The platform `fetch` is read when a call is made, not when the client is imported
|
|
183
|
-
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
|
|
184
193
|
resolved.
|
|
185
194
|
|
|
186
195
|
## Up to date
|
|
187
196
|
|
|
188
|
-
A rerun asks the deployment conditionally (`If-None-Match`, the ClientContract hash of the output it
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
191
200
|
`avclient: up to date: … nothing was written.` and exit 0; otherwise the output is replaced, and the line says
|
|
192
201
|
`the ClientContract is unchanged; the generator or the entrypoint changed`.
|
|
193
202
|
|
|
194
203
|
## What is emitted
|
|
195
204
|
|
|
196
205
|
Every file opens with a banner stating that it is generated and must not be edited, and with the Biome suppressions a
|
|
197
|
-
generated file needs
|
|
198
|
-
|
|
199
|
-
`generated/types.d.ts`; the `.ts` sources carry their own types.
|
|
200
|
-
|
|
201
|
-
- `AvClient.ts
|
|
202
|
-
`
|
|
203
|
-
subclasses, `
|
|
204
|
-
- `generated/client.ts
|
|
205
|
-
instance `avClient
|
|
206
|
-
- `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,
|
|
207
216
|
as the type `ClientContractShape` (no runtime byte).
|
|
208
|
-
- `generated/derivation
|
|
209
|
-
|
|
210
|
-
- `generated/enums.ts
|
|
211
|
-
- `generated/metadata.ts
|
|
212
|
-
was generated from, its default deployment (an entrypoint carrying credentials is refused at resolution).
|
|
213
|
-
- `generated/runtime/codec.ts
|
|
214
|
-
- `generated/runtime/decimal.ts
|
|
215
|
-
- `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
|
|
216
225
|
relations it follows (the decode table), and the advertised operations.
|
|
217
|
-
- `generated/runtime/errors.ts
|
|
218
|
-
`
|
|
219
|
-
the code unions, emitted from core's code arrays
|
|
220
|
-
- `generated/runtime/
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
- `generated/
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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.
|
|
234
251
|
|
|
235
252
|
## What a generated client is bound to
|
|
236
253
|
|
|
237
|
-
A generated client is bound to the
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
`
|
|
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`.
|
|
241
258
|
|
|
242
|
-
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
|
|
243
260
|
different ClientContracts, and a client generated against one is refused by the other. Whatever the cause, the remedy
|
|
244
|
-
is the same
|
|
261
|
+
is the same: generate against the deployment the client will call, and regenerate when it changes.
|
|
245
262
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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.
|
|
250
267
|
|
|
251
|
-
The embedded entrypoint is a
|
|
252
|
-
|
|
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.
|
|
253
270
|
|
|
254
271
|
## Type-checking cost in a consumer
|
|
255
272
|
|
|
256
|
-
|
|
257
|
-
|
|
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
|
|
258
275
|
`skipLibCheck: false` every named type is resolved where it is declared, so the cost grows with the number of
|
|
259
276
|
Resources.
|
|
260
277
|
|
|
261
278
|
## Before 1.0
|
|
262
279
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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.
|
|
266
283
|
|
|
267
284
|
## Known issues
|
|
268
285
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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:
|
|
273
290
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
291
|
+
```ts
|
|
292
|
+
export default defineClientConfig({
|
|
293
|
+
entrypoint: env("AVENTARA_API_URL"),
|
|
294
|
+
generateAt: "./src/api",
|
|
295
|
+
tsconfigFile: "./tsconfig.app.json",
|
|
296
|
+
});
|
|
297
|
+
```
|
|
281
298
|
|
|
282
299
|
## License
|
|
283
300
|
|
|
284
|
-
PolyForm Shield 1.0.0 with an additional permission
|
|
285
|
-
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,5 +1,5 @@
|
|
|
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 .
|
|
2
|
-
export declare const GENERATE_USAGE = "Usage: avclient generate [--yes]\n\nRead framework.client.ts (or .
|
|
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 .mts, .cts, .js, .mjs,\n .cjs) 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
|
+
export declare const GENERATE_USAGE = "Usage: avclient generate [--yes]\n\nRead framework.client.ts (or .mts, .cts, .js, .mjs, .cjs) 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
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";
|
|
@@ -22,11 +22,11 @@ export type CliCommand = {
|
|
|
22
22
|
readonly yes: boolean;
|
|
23
23
|
} | ClientInitCommand;
|
|
24
24
|
/** The command line cannot be understood. A refusal: one sentence, exit 1. */
|
|
25
|
-
export declare class
|
|
26
|
-
readonly name = "
|
|
25
|
+
export declare class AvCliCommandError extends Error {
|
|
26
|
+
readonly name = "AvCliCommandError";
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* @throws
|
|
29
|
+
* @throws AvCliCommandError when `argv` is not `generate [--yes|-y]`, `init …`,
|
|
30
30
|
* `--help`/`-h` or `--version`/`-v`.
|
|
31
31
|
*/
|
|
32
32
|
export declare function parseCliCommand(argv: readonly string[]): CliCommand;
|
|
@@ -2,8 +2,8 @@ import { CLIENT_CONFIG_FILE } from "../config/config.loader.js";
|
|
|
2
2
|
export const USAGE = `avclient — generate a typed Aventara client from a deployed ClientContract.
|
|
3
3
|
|
|
4
4
|
Usage:
|
|
5
|
-
avclient generate [--yes] Read ${CLIENT_CONFIG_FILE} (or .
|
|
6
|
-
.
|
|
5
|
+
avclient generate [--yes] Read ${CLIENT_CONFIG_FILE} (or .mts, .cts, .js, .mjs,
|
|
6
|
+
.cjs) in the current directory, fetch
|
|
7
7
|
<entrypoint>/_contract, and write AvClient.ts and
|
|
8
8
|
generated/ into its generateAt directory, its imports
|
|
9
9
|
spelled as the project's tsconfig.json says.
|
|
@@ -39,7 +39,7 @@ current directory, highest precedence first: the process environment,
|
|
|
39
39
|
`;
|
|
40
40
|
export const GENERATE_USAGE = `Usage: avclient generate [--yes]
|
|
41
41
|
|
|
42
|
-
Read ${CLIENT_CONFIG_FILE} (or .
|
|
42
|
+
Read ${CLIENT_CONFIG_FILE} (or .mts, .cts, .js, .mjs, .cjs) in the current directory,
|
|
43
43
|
fetch <entrypoint>/_contract, and write AvClient.ts and generated/ into its
|
|
44
44
|
generateAt directory, its imports spelled as the project's tsconfig.json says.
|
|
45
45
|
|
|
@@ -75,14 +75,14 @@ Options:
|
|
|
75
75
|
On a terminal every unanswered question is asked; anywhere else, pass its flag
|
|
76
76
|
or --yes, or the run stops before writing anything.
|
|
77
77
|
`;
|
|
78
|
-
export class
|
|
79
|
-
name = "
|
|
78
|
+
export class AvCliCommandError extends Error {
|
|
79
|
+
name = "AvCliCommandError";
|
|
80
80
|
}
|
|
81
81
|
export function parseCliCommand(argv) {
|
|
82
82
|
const [command, ...rest] = argv;
|
|
83
83
|
const help = "run `avclient --help` for usage.";
|
|
84
84
|
if (command === undefined) {
|
|
85
|
-
throw new
|
|
85
|
+
throw new AvCliCommandError(`a command is required; ${help}`);
|
|
86
86
|
}
|
|
87
87
|
if (command === "--help" || command === "-h") {
|
|
88
88
|
return { command: "help" };
|
|
@@ -98,7 +98,7 @@ export function parseCliCommand(argv) {
|
|
|
98
98
|
return parseInit(rest, help);
|
|
99
99
|
}
|
|
100
100
|
if (command !== "generate") {
|
|
101
|
-
throw new
|
|
101
|
+
throw new AvCliCommandError(`unknown command ${JSON.stringify(command)}; ${help}`);
|
|
102
102
|
}
|
|
103
103
|
let yes = false;
|
|
104
104
|
for (const argument of rest) {
|
|
@@ -106,7 +106,7 @@ export function parseCliCommand(argv) {
|
|
|
106
106
|
yes = true;
|
|
107
107
|
continue;
|
|
108
108
|
}
|
|
109
|
-
throw new
|
|
109
|
+
throw new AvCliCommandError(`unexpected argument ${JSON.stringify(argument)}: generate takes only --yes, ` +
|
|
110
110
|
`everything else it needs is in ${CLIENT_CONFIG_FILE}; ${help}`);
|
|
111
111
|
}
|
|
112
112
|
return { command: "generate", yes };
|
|
@@ -146,16 +146,16 @@ function parseInit(argv, help) {
|
|
|
146
146
|
const value = INIT_VALUES[flag];
|
|
147
147
|
const toggle = INIT_SWITCHES[flag];
|
|
148
148
|
if (value === undefined && toggle === undefined) {
|
|
149
|
-
throw new
|
|
149
|
+
throw new AvCliCommandError(`unexpected argument ${JSON.stringify(argument)} for init; ${help}`);
|
|
150
150
|
}
|
|
151
151
|
const canonical = toggle === "yes" ? "--yes" : flag;
|
|
152
152
|
if (seen.has(canonical)) {
|
|
153
|
-
throw new
|
|
153
|
+
throw new AvCliCommandError(`${canonical} was given twice; ${help}`);
|
|
154
154
|
}
|
|
155
155
|
seen.add(canonical);
|
|
156
156
|
if (toggle !== undefined) {
|
|
157
157
|
if (inline !== undefined) {
|
|
158
|
-
throw new
|
|
158
|
+
throw new AvCliCommandError(`${canonical} takes no value; ${help}`);
|
|
159
159
|
}
|
|
160
160
|
switches[toggle] = true;
|
|
161
161
|
continue;
|
|
@@ -163,7 +163,7 @@ function parseInit(argv, help) {
|
|
|
163
163
|
const taken = inline ?? argv[index + 1];
|
|
164
164
|
if (taken === undefined ||
|
|
165
165
|
(inline === undefined && taken.startsWith("-"))) {
|
|
166
|
-
throw new
|
|
166
|
+
throw new AvCliCommandError(`${canonical} takes a value; ${help}`);
|
|
167
167
|
}
|
|
168
168
|
if (inline === undefined) {
|
|
169
169
|
index += 1;
|
|
@@ -171,7 +171,7 @@ function parseInit(argv, help) {
|
|
|
171
171
|
given[value] = taken;
|
|
172
172
|
}
|
|
173
173
|
if (switches.noEnvVar && given.envVar !== undefined) {
|
|
174
|
-
throw new
|
|
174
|
+
throw new AvCliCommandError(`--env-var and --no-env-var contradict each other; ${help}`);
|
|
175
175
|
}
|
|
176
176
|
return {
|
|
177
177
|
command: "init",
|