@aventara/client 1.0.0-rc.13 → 1.0.0-rc.14
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
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @aventara/client
|
|
2
2
|
|
|
3
|
+
## 1.0.0-rc.14
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 0895a93: `avDehydrate` and `avHydrate` are typed: data handed from a Server Component to a Client Component as props keeps its type, with no cast.
|
|
8
|
+
|
|
9
|
+
- `avDehydrate(data)` (`@aventara/next`) returns an `AvDehydrated<T>` for data of type `T`, and `avHydrate(value)` (`@/aventara/client`) of an `AvDehydrated<T>` returns the `T`, its `Date`, `bigint`, `Uint8Array` and `AvDecimal` values included. Type an SWR `fallback` prop `AvDehydrated<T>` and pass `avHydrate(fallback)` to `SWRConfig` as it is.
|
|
10
|
+
- `AvDehydrated<T>` is new, exported by `@aventara/next` and by the `src/aventara/client.ts` that `aventara-build --client` writes; the two are one type. It is opaque: a plain object, the data itself, or the dehydrated form of another type is a compile error where an `AvDehydrated<T>` is expected.
|
|
11
|
+
- A Resource or enum named `AvDehydrated` or `avHydrate` is now emitted under a renamed identifier (`AvDehydratedModel`, `avHydrateModel`), with a warning, as other names the generated client owns are. Before, `src/aventara/client.ts`'s own declaration hid a Resource type of that name, which could not be imported from it.
|
|
12
|
+
- Read data you dehydrate through `@/aventara/universal`: `@/aventara/server` types a decimal as core's `AvDecimal`, while `avHydrate` revives it as the browser client's.
|
|
13
|
+
- Pass `avHydrate(fallback)` to `SWRConfig`, never the dehydrated value itself: SWR's `fallback` is typed `any`, so the compiler cannot catch it.
|
|
14
|
+
- `avHydrate` still takes `unknown` and returns `unknown`; `avDehydrate` of an `unknown` value returns an `AvDehydrated<unknown>`. `serializeData: avDehydrate` and `deserializeData: avHydrate` in TanStack Query are written as before.
|
|
15
|
+
- The agent skills show the SWR props pattern typed, with no cast. The frontend skill's refresh line no longer names `npx`, and says a file you edited is kept with a warning naming it, and replaced with `--force`.
|
|
16
|
+
|
|
17
|
+
- ec0d332: `avclient skill` no longer ends with "the client skill is up to date" after keeping a file you edited: it says the skill is up to date apart from what the warnings above name, as `aventara skill` does. The server skill now says what the frontend skill holds: a `SKILL.md` with `reference/client.md`'s content, and `reference/client-setup.md`.
|
|
18
|
+
|
|
19
|
+
- Updated dependencies [1c5eaa3]
|
|
20
|
+
- @aventara/core@1.0.0-rc.14
|
|
21
|
+
|
|
3
22
|
## 1.0.0-rc.13
|
|
4
23
|
|
|
5
24
|
### Minor Changes
|
|
@@ -65,6 +84,7 @@
|
|
|
65
84
|
- With `@aventara/access` installed, the skill now says to write Access's own skill with `avaccess skill` (`npx avaccess skill`, or `pnpm exec avaccess skill`).
|
|
66
85
|
- Corrected: a write-only field is written `"read"` everywhere; a field pipe or compute stage that returns `undefined` keeps the value; a read compute may be a plain function and gets `field`, `value`, `record` and `ctx`.
|
|
67
86
|
- `CONFIG_FORBIDDEN_IN_OVERLAY` in the error catalog now names `managedElsewhere` as well as a field `definition`.
|
|
87
|
+
|
|
68
88
|
- Updated dependencies [6c3e25c]
|
|
69
89
|
- Updated dependencies [2aaa27d]
|
|
70
90
|
- Updated dependencies [f3b291f]
|
package/dist/cli.js
CHANGED
|
@@ -36,7 +36,7 @@ export async function runCli(argv, io) {
|
|
|
36
36
|
for (const warning of skill.warnings) {
|
|
37
37
|
io.stderr(`avclient: warning: ${warning}.\n`);
|
|
38
38
|
}
|
|
39
|
-
io.stdout(`avclient: ${skill.written.length === 0 ?
|
|
39
|
+
io.stdout(`avclient: ${skill.written.length === 0 ? `the client skill is up to date${skill.warnings.length === 0 ? "" : ", apart from what the warnings above name"}` : `wrote ${skill.written.join(", ")}`}.\n`);
|
|
40
40
|
return 0;
|
|
41
41
|
}
|
|
42
42
|
return await runGenerate(io, command.yes);
|
|
@@ -11,6 +11,7 @@ export const RESERVED_EMITTED_NAMES = [
|
|
|
11
11
|
"AvCallOptions",
|
|
12
12
|
"AvCause",
|
|
13
13
|
"AvDecimal",
|
|
14
|
+
"AvDehydrated",
|
|
14
15
|
"AvFetch",
|
|
15
16
|
"AvError",
|
|
16
17
|
"AvNotFoundError",
|
|
@@ -22,6 +23,7 @@ export const RESERVED_EMITTED_NAMES = [
|
|
|
22
23
|
"AvValidationCode",
|
|
23
24
|
"AvValidationIssue",
|
|
24
25
|
"avClient",
|
|
26
|
+
"avHydrate",
|
|
25
27
|
"avLazy",
|
|
26
28
|
...FRAMEWORK_ERROR_CLASS_NAMES,
|
|
27
29
|
]),
|
|
@@ -8,4 +8,9 @@ export declare const HYDRATION_FORMAT: {
|
|
|
8
8
|
readonly objectTag: "object";
|
|
9
9
|
/** The scalars a value is tagged as: those whose runtime values JSON does not carry. */
|
|
10
10
|
readonly scalarTags: readonly ["bigint", "bytes", "datetime", "decimal"];
|
|
11
|
+
/**
|
|
12
|
+
* The key of `AvDehydrated<T>`'s brand: a property of its type only, never
|
|
13
|
+
* of the value, holding the `T` the value was dehydrated from.
|
|
14
|
+
*/
|
|
15
|
+
readonly typeKey: "~avDehydrated";
|
|
11
16
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export const CLIENT_SKILL_FILES = {
|
|
2
|
-
"SKILL.md": "---\nname: aventara-client\ndescription: Call this frontend's Aventara API through the generated client (avclient generate, AvClient.ts; @/aventara/client in Next.js). Use when calling operations (find, create, update, delete, upsert), reading results (Date, bigint, AvDecimal) and errors (AvValidationError, cause.issues, AvTransportError), writing transactions (avClient.transaction, $ref), loading several calls at once (Promise.all, avClient.all, avLazy), aborting calls, reading cached reads, editing framework.client.ts (defineClientConfig, env, AVENTARA_API_URL, generateAt), or regenerating the client after the server changed (avclient generate, A2005, AvContractMismatchError).\n---\n# Calling the API\n\nHow frontend and server code call the operations: several calls in one round trip, aborting, `avLazy`, and how cached reads go out. The generated client is the default export of the file `avclient generate` writes (`api/AvClient.ts`, at the frontend's `generateAt`); in a Next.js app it is `@/aventara/client` in the browser and `@/aventara/server` on the server.\n\n- `reference/client-setup.md`: `framework.client.ts`, pointing the client at a server, regenerating it after the server changed, CI, and the stale-client error (`A2005`).\n\nFor anything not covered here, read the docs, which are Markdown for agents: `https://aventara.razor34.dev/framework/llms.txt` lists every page, `https://aventara.razor34.dev/framework/markdown/<slug>` is one page (`…/markdown/error-codes` explains every code).\n\n## The query language is not Prisma's\n\nThe types catch each of these; knowing them saves a round per guess.\n\n| Prisma | Aventara |\n|---|---|\n| `select: { id: true, title: true }` | `select: [\"id\", \"title\"]`; a relation is `include: { author: { select: [\"name\"] } }` |\n| `take`, `skip` | `limit`, `offset`; `cursor: { id }` needs an explicit `orderBy` (`V1009` without one) |\n| `_count` | `\"$count\"` in a `select` list, or `find.count({ where })` |\n| `connect`, `create`, `set` (relations) | `$connect`, `$create`, `$connectOrCreate`, `$disconnect`, `$set`, `$update`, `$delete`, `$upsert` |\n| `increment`, `push` | `{ $increment: 1 }`, `$decrement`, `$multiply`, `$divide`, `$push` |\n| `undefined` in a `where` (ignored) | refused (`V1001`): leave the key out |\n| `findMany`, `update` | `find.many`, `update.unique`; `update.first` writes one row a `where` matches |\n\n## What a call returns\n\nA call resolves to the data: the record, the list or the count. `find.first`, `update.first` and `delete.first` resolve `null` when no row matches; a `unique` operation that matches no row throws `AvNotFoundError` (`A2003`).\n\n| Prisma type | In arguments and results |\n|---|---|\n| `DateTime` | `Date` |\n| `BigInt` | `bigint` |\n| `Decimal` | `AvDecimal`, from the generated client: `new AvDecimal(\"12.50\")`, read with `toString()`; it does no arithmetic |\n| `Bytes` | `Uint8Array` |\n| `Json` | the JSON value as it is |\n\nThe client converts to and from the strings on the wire for you. To type a component's props, import the generated types: each Resource's (`Post`, `PostWhere`, `PostCreateData`, …, each only when the operation it belongs to is exposed) and each enum, as a type and an `as const` object.\n\n## Errors\n\nEvery outcome that is not a success throws. Branch on the class or on `error.code`, never on the message. Server code (`framework.application`, `ctx` calls) throws the same classes from `@aventara/core`.\n\n| Class | Codes | When |\n|---|---|---|\n| `AvValidationError` | `A2004`, `A2007`, `A2009` | The arguments break a rule, a transaction `$ref` is wrong, or a limit is crossed. |\n| `AvNotFoundError` | `A2003` | A `unique` operation matched no row. |\n| `AvConflictError` | `A2008`, `A2013`, `A2014` | A duplicate or a missing parent (`A2008`), or the row changed under the write (`A2013`, `A2014`: read again and retry). |\n| `AvAuthError` | `A4000`, `A4001`, `A4002` | Sign in first (`A4000`), or not allowed (`A4001`, `A4002`). |\n| `AvContractMismatchError` | `A2005` | The client is stale: regenerate it (`reference/client-setup.md`). |\n| `AvProtocolError` | `A2000`, `A2001`, `A2002`, `A2006`, `A2010`, `A2011`, `A2012` | A request the server cannot read or route. |\n| `AvInternalError` | `A3000`, `A3001`, `A3002`, `A3003`, `A3004` | The server failed; the real error is in its log. |\n| `AvTransportError` | none | No Aventara answer arrived: the network failed, or a proxy answered with its own page. `error.status` is the HTTP status, or `null`. |\n\n`AvError` is the base class of every class in the table but `AvTransportError`. A `$access` call (Aventara Access) may also throw `AvAccessError`, which is not an `AvError`.\n\nAn `AvValidationError` lists each problem in `error.cause.issues`: `{ path, code, message }`, plus `check` and `params` for a failed field validator (`check: \"max_length\"`, `params: { maximum: 80 }`). `path` is where in the call (`[\"arguments\", \"data\", \"email\"]`), so a form can show `message` beside the field `path` ends with. The client runs the server's field validators before sending: a value they refuse throws this same error, and nothing is sent.\n\n```ts\nimport avClient, { AvValidationError } from \"./api/AvClient\";\n\ntry {\n await avClient.User.create.one({ data: { email: \"not-an-email\" } });\n} catch (error) {\n if (!(error instanceof AvValidationError)) throw error;\n for (const issue of error.cause.issues ?? []) console.log(issue.path?.at(-1), issue.message);\n}\n```\n\n## Calls made together share one request\n\nCalls made in the same tick (before the microtask queue settles) go out as one `POST <api>/_all`, an Operation Set. Each call still resolves or throws on its own. Write nothing for it: no batching endpoint, no loader.\n\n```ts\nimport avClient from \"./api/AvClient\";\n\nconst [posts, published] = await Promise.all([\n avClient.Post.find.many({ where: { published: true }, select: [\"id\", \"title\"] }),\n avClient.Post.find.count({ where: { published: true } }),\n]); // one POST /api/_all\n```\n\n- Each call runs exactly as alone: validation, exposure, policy, limits and pipelines are those of its own route, and so is its answer.\n- Not atomic and in no order: the calls run concurrently, and a write that ran stays written whatever the others answered. Writes that must hold together, or a call that needs another's result, belong in a transaction (`avClient.transaction([avClient.tx.Post.create.one(…), …])`, `POST /_transactions`).\n- Reads and writes mix. A group larger than `limits.maxAll` (10) or `limits.maxRequestBytes` is split by the client.\n- Always sent alone: a call with `headers` or `requestId` in its options, a call or client with `grouping: false`, a transaction, and a cached read (a `GET`). Headers every call needs (a token) go in `onCall`, set once, `new AvClient({ onCall })`, so the calls still group (see \"Hooks\" below); calls whose `onCall` additions differ never share a request.\n- A server older than rc.11 answers `/_all` with `A2002`; the client resends the calls one by one and stops grouping.\n\n## Transactions\n\nWrites that must commit together, or a step that uses an earlier step's result, go in one transaction:\n\n```ts\nimport avClient from \"./api/AvClient\";\n\nconst author = avClient.tx.User.create.one({ data: { email: \"ada@example.com\" }, select: [\"id\"] });\nconst post = avClient.tx.Post.create.one({ data: { title: \"Hello\", authorId: author.$ref(\"id\") }, select: [\"id\"] });\nconst [user, created] = await avClient.transaction([author, post]); // one result per step, typed, in order\n```\n\n- `avClient.tx.<Resource>.<family>.<variant>(args)` describes a step and sends nothing. `avClient.transaction([...])` sends the steps once, as one `POST <api>/_transactions`; they all commit or none does.\n- `step.$ref(\"field\")` stands for that field of an earlier step's result, on a step whose result is one row (`create.one`, `find.unique`, `update.unique`, …). The field must be in that step's result: put it in its `select` (a field the result does not carry is refused, `A2007` / `V1010`). Write it in `data` at any depth (a field, a `$connect`), in a `where` or in a `cursor`, never in a projection's nested `where` or `orderBy`. Its type must fit where it is written; a mismatch is a type error.\n- A failing step rolls every step back and throws its error, with `error.cause.operation` the failing step's index.\n- The client has no `tx` when the server's client layer sets `transactionsStyle: \"server\"` or `\"none\"`.\n- Server code: `framework.transaction([framework.appTx.…])`, the same `$ref`s. In Next.js, `avApp.appTx` with `avApp.transaction([...])`, or `avClient.tx` with `avClient.transaction([...])` from `@/aventara/server`, never mixed in one list.\n\n## Hooks: `onCall`, `onSuccess`, `onError`\n\nThree `AvClient` options see every call: operations, cached reads, a transaction (once), `$access` calls. `onCall` adds to the request; `onSuccess` and `onError` only watch.\n\n```ts\nimport { AvAuthError, AvClient } from \"./api/AvClient\";\n\nexport const authed = new AvClient({\n onCall: () => ({ headers: { Authorization: `Bearer ${localStorage.getItem(\"token\") ?? \"\"}` }, credentials: \"include\" }),\n onError: (_call, error) => {\n if (error instanceof AvAuthError) location.assign(\"/login\");\n },\n});\n```\n\n- `onCall(call)` runs when the call is made, before grouping, and may return or resolve to `{ headers, credentials, cache, keepalive, priority, referrerPolicy }`. Calls with equal additions share one `/_all`. An async one holds back only its own call: it is sent when its `onCall` settles, with the calls queued then, so calls group when their `onCall` settles together with equal additions. An `onCall` doing its own I/O per call (reading the token again each time) sends each call alone; to keep grouping, await one shared in-flight promise, cleared once it settles: `let inFlight; const token = () => (inFlight ??= readToken().finally(() => { inFlight = undefined; }));`. If `token()` rejects, the call is sent without `Authorization` (the hook warning fires) and will likely reach `onError` as a refusal. A hanging `onCall` stalls only its own call, and an abort settles that call at once. A call made inside a hook never waits on the call that ran it.\n- Order: `onCall`'s headers, then the call's own `headers` option (wins), then the client's own (always win). `Content-Type`, `Accept` and the `Aventara-*` headers (canonical or lower-case), and `method`, `body`, `signal`, `redirect`, `mode`, are type errors in `onCall`; at runtime those headers in any case, invalid names and values with a line break, a control character, or a character outside Latin-1 are dropped with a warning (`AVENTARA_CLIENT_HOOK_HEADER_DROPPED`), once per header name for each `onCall`.\n- `call` is `{ kind, label, resource, family, variant, requestId? }` for an operation (`kind: \"transaction\"` or `\"access\"` with `verb` otherwise); never the arguments. To type a hook declared on its own, import `AvCall` and `AvCallAdditions` from the generated client.\n- `onSuccess(call, result)` and `onError(call, error)` are sinks returning `void` (any function fits, async too): side effects only, what they return is ignored and never awaited, so they never delay the caller, and they cannot swallow, change or retry a call. `onError` sees refusals, validation errors, transport failures (`AvTransportError`) and aborts, grouped or alone.\n- A hook that throws or rejects is reported once as a warning (`AVENTARA_CLIENT_HOOK_THREW`) and skipped; the call goes on, with no additions when it was `onCall`.\n- `fetch` only swaps the transport (tests, another runtime's fetch): it must return the `Response` unread. Do not put headers or a token in it. An entrypoint URL carrying credentials (`https://user:pass@…`) is refused by `avclient`: add them through `onCall`.\n- Next.js: put the hooks in `src/aventara/client-options.ts` (or `.js`; no other extension) as `export default { onCall, onSuccess, onError } satisfies Pick<AvClientOptions, \"onCall\" | \"onSuccess\" | \"onError\">` (type from `@/aventara/client`), then run `aventara:prepare` (it runs `aventara-build --client`). Both `@/aventara/client` and `@/aventara/universal` use them, reading the default export when a hook runs, so the file may value-import from `@/aventara/client` (an error class); `fetch`, `grouping` and `entrypoint` there are type errors. A file with no default export leaves the client without hooks, with one warning (`AVENTARA_CLIENT_OPTIONS_NO_DEFAULT`). The file is bundled into the browser: no secrets or server-only imports; branch on `typeof window`. On the server, onCall's headers reach the in-process request but not `ctx.$state`, which comes from the request being served. `@/aventara/server` is not an AvClient and takes no hooks.\n\n## Calls run when you make them\n\nA call is checked, encoded and queued when it is made, as `fetch` is: a call never awaited still runs, and awaiting it twice sends it once. To describe a call now and make it later, wrap it in `avLazy`:\n\n```ts\nimport avClient, { avLazy } from \"./api/AvClient\";\n\nconst drafts = avLazy(() => avClient.Post.find.many({ where: { published: false }, select: [\"id\"] })); // nothing sent\nconst list = await drafts; // sent now\n```\n\n| Where `avLazy` comes from | Awaited again |\n|---|---|\n| The generated client (browser) | Keeps its one answer. |\n| `@aventara/core` (server code) | Calls again each time; it keeps no answer, so never hold one in module scope expecting a cache. |\n| Next.js `@/aventara/universal` on the server | Keeps its answer within one request's React render only. |\n\n## `avClient.all`\n\nAn object of calls in, one typed value per key out. The keys are yours and never sent.\n\n```ts\nimport avClient, { AvNotFoundError } from \"./api/AvClient\";\n\n// Strict (default): the first failure rejects `all` with that error and aborts its other calls.\nconst { list, total } = await avClient.all({\n list: avClient.Post.find.many({ select: [\"id\", \"title\"] }),\n total: avClient.Post.find.count({}),\n});\n\n// settle: true: every key settles on its own; `all` never rejects for one key.\nconst panels = await avClient.all(\n {\n posts: avClient.Post.find.many({ select: [\"id\", \"title\"] }),\n me: avClient.User.find.unique({ where: { email: \"ada@example.com\" }, select: [\"id\", \"email\"] }),\n },\n { settle: true },\n);\nif (panels.me.status === \"rejected\") {\n if (panels.me.reason instanceof AvNotFoundError) console.log(\"no such user\");\n} else {\n console.log(panels.me.value.email, list.length, total);\n}\n```\n\nStrict is fail-fast, not atomic: a write that already ran stays written. Use strict when the page needs every value, `settle: true` when each panel renders or fails alone. Every entry must be a call (or an `avLazy`); a plain value does not compile.\n\n## Aborting\n\nEach call has its own abort: `{ signal }` in a call's options aborts that call only; `avClient.all({ … }, { signal })` aborts every call of that `all`, and a call inside it stops on whichever signal fires first.\n\n```ts\nimport avClient from \"./api/AvClient\";\n\nconst page = new AbortController();\nconst search = new AbortController();\n\nconst result = avClient.all(\n {\n hits: avClient.Post.find.many({ where: { title: { contains: \"rc\" } } }, { signal: search.signal }),\n count: avClient.Post.find.count({}),\n },\n { signal: page.signal },\n);\nsearch.abort(); // only `hits` stops; strict `all` then rejects with its AbortError\n```\n\n- Aborted before the request is sent: the call is dropped. After: it rejects at once; the other calls of the same request go on.\n- Calls grouped into the same request but not in that `all` are never touched. The HTTP request itself is aborted only when every call in it is.\n- A started database query still finishes on the server (no cancel); a started write is written.\n- Nothing aborts for you: no default timeout, no abort on navigation. Pass the signal your data library gives (TanStack Query's `queryFn` receives one: `queryFn: ({ signal }) => avClient.Post.find.many({}, { signal })`), or abort in an effect's cleanup.\n\n## Turning grouping off\n\n```ts\nimport avClient, { AvClient } from \"./api/AvClient\";\n\nawait avClient.Post.find.many({ select: [\"id\"] }, { grouping: false }); // this call, on its own route\nconst plain = new AvClient({ grouping: false }); // every call of this client\nawait plain.Post.find.count({});\n```\n\nOnly for a reason: debugging in the network tab, per-route monitoring or rate limits, or a proxy that lets through `/_resources/…` but not `/_all`. A transaction takes no `grouping` option.\n\n## On the server\n\nServer code has the same `all`: `framework.serverClient.all` (NestJS, any host) and `avClient.all` from `@/aventara/server` (Next.js). `framework.application` and `avApp` have no `all`.\n\n```tsx\n// src/app/dashboard/page.tsx\nimport { avLazy } from \"@aventara/core\";\nimport { avClient } from \"@/aventara/server\";\n\nexport default async function Dashboard() {\n const { posts, published } = await avClient.all({\n posts: avClient.Post.find.many({ select: [\"id\", \"title\"] }),\n published: avLazy(() => avClient.Post.find.count({ where: { published: true } })),\n });\n return <p>{posts.length} posts, {published} published</p>;\n}\n```\n\n- Nothing is grouped and nothing goes over HTTP: each call runs in process, in the request that made it, with that request's state and principal (`framework.serverClient` calls take `{ state }` like any server call).\n- Any number of entries: `limits.maxAll` guards `POST /_all` only.\n- A plain call has already started when made, so `all` can only await it. An `avLazy` entry is started by `all`: at most `limits.maxAllConcurrency` (4) at once, in key order, and none after a strict `all` failed or was aborted.\n- Next.js `@/aventara/universal` groups in the browser only; on the server each of its calls runs on its own.\n\n## The two limits\n\n| Limit | Default | Bounds |\n|---|---|---|\n| `limits.maxAll` | `10` | Operations in one `POST /_all`. More is refused whole: `422 A2009` / `V1014`. The generated client splits for you. |\n| `limits.maxAllConcurrency` | `4` | Operations of one set (HTTP or in process) running at once; the rest wait, in key order. At least `1`. |\n\nOrdinary limits: set them at the root, override them in `client` or `application` (`limits: { maxAll: 20 }`). Do not grow the database pool for `/_all`; the concurrency limit keeps one request to its share.\n\n## Cached reads from the client\n\nA read the server caches (its Resource's `cache` stage) is sent by the generated client as `GET <api>/_resources/<R>/find/<variant>?q=<args>&v=<contract hash>`, alone, never in an Operation Set. Leave `requestId` off a cached cross-origin read (it adds a CORS preflight). A URL over 4096 characters goes as a `POST` and is not cached. The `POST` route stays for any client.\n\n## By hand (curl, a custom client)\n\n`POST <api>/_all` takes the two identity headers and `{ \"operations\": { \"<key>\": { \"resource\": \"Post\", \"family\": \"find\", \"variant\": \"many\", \"args\": { … } } } }` (keys are identifiers; each entry is exactly those four members). It answers `200 A1000` with `data` holding each key's own envelope (`{ data, code, cause }`): read each key's `code`, not the status. With `Accept: application/x-ndjson` it streams one line per entry as it finishes, `{\"key\":\"…\",\"envelope\":{…}}`; the generated client does this for you, so write it only in a client of your own. Too many entries: `422 A2009`; none: `422 A2004`.\n\nA plan of your own, `POST <api>/_transactions` with `{ \"operations\": [ … ] }`, needs each step's `fingerprint`: compute it with `await AvProtocol.fingerprint({ resource, family, variant, args })` from `@aventara/core/protocol` over the step's wire-form `args`, the function the server checks it with. It is async: a step whose `fingerprint` is the unawaited promise serializes as `{}`, and a wrong fingerprint answers `422 A2007` / `V1017`. A layer with `transactionsStyle: \"server\"` or `\"none\"` has no such route (`404 A2002`).\n\n## Keeping this file current\n\nGenerated by `avclient` {{CLIENT_VERSION}}. After upgrading `@aventara/client`, refresh it: `npx avclient skill`. A copy you edited is kept (pass `--force` to replace it).\n",
|
|
2
|
+
"SKILL.md": "---\nname: aventara-client\ndescription: Call this frontend's Aventara API through the generated client (avclient generate, AvClient.ts; @/aventara/client in Next.js). Use when calling operations (find, create, update, delete, upsert), reading results (Date, bigint, AvDecimal) and errors (AvValidationError, cause.issues, AvTransportError), writing transactions (avClient.transaction, $ref), loading several calls at once (Promise.all, avClient.all, avLazy), aborting calls, reading cached reads, editing framework.client.ts (defineClientConfig, env, AVENTARA_API_URL, generateAt), or regenerating the client after the server changed (avclient generate, A2005, AvContractMismatchError).\n---\n# Calling the API\n\nHow frontend and server code call the operations: several calls in one round trip, aborting, `avLazy`, and how cached reads go out. The generated client is the default export of the file `avclient generate` writes (`api/AvClient.ts`, at the frontend's `generateAt`); in a Next.js app it is `@/aventara/client` in the browser and `@/aventara/server` on the server.\n\n- `reference/client-setup.md`: `framework.client.ts`, pointing the client at a server, regenerating it after the server changed, CI, and the stale-client error (`A2005`).\n\nFor anything not covered here, read the docs, which are Markdown for agents: `https://aventara.razor34.dev/framework/llms.txt` lists every page, `https://aventara.razor34.dev/framework/markdown/<slug>` is one page (`…/markdown/error-codes` explains every code).\n\n## The query language is not Prisma's\n\nThe types catch each of these; knowing them saves a round per guess.\n\n| Prisma | Aventara |\n|---|---|\n| `select: { id: true, title: true }` | `select: [\"id\", \"title\"]`; a relation is `include: { author: { select: [\"name\"] } }` |\n| `take`, `skip` | `limit`, `offset`; `cursor: { id }` needs an explicit `orderBy` (`V1009` without one) |\n| `_count` | `\"$count\"` in a `select` list, or `find.count({ where })` |\n| `connect`, `create`, `set` (relations) | `$connect`, `$create`, `$connectOrCreate`, `$disconnect`, `$set`, `$update`, `$delete`, `$upsert` |\n| `increment`, `push` | `{ $increment: 1 }`, `$decrement`, `$multiply`, `$divide`, `$push` |\n| `undefined` in a `where` (ignored) | refused (`V1001`): leave the key out |\n| `findMany`, `update` | `find.many`, `update.unique`; `update.first` writes one row a `where` matches |\n\n## What a call returns\n\nA call resolves to the data: the record, the list or the count. `find.first`, `update.first` and `delete.first` resolve `null` when no row matches; a `unique` operation that matches no row throws `AvNotFoundError` (`A2003`).\n\n| Prisma type | In arguments and results |\n|---|---|\n| `DateTime` | `Date` |\n| `BigInt` | `bigint` |\n| `Decimal` | `AvDecimal`, from the generated client: `new AvDecimal(\"12.50\")`, read with `toString()`; it does no arithmetic |\n| `Bytes` | `Uint8Array` |\n| `Json` | the JSON value as it is |\n\nThe client converts to and from the strings on the wire for you. To type a component's props, import the generated types: each Resource's (`Post`, `PostWhere`, `PostCreateData`, …, each only when the operation it belongs to is exposed) and each enum, as a type and an `as const` object.\n\n## Errors\n\nEvery outcome that is not a success throws. Branch on the class or on `error.code`, never on the message. Server code (`framework.application`, `ctx` calls) throws the same classes from `@aventara/core`.\n\n| Class | Codes | When |\n|---|---|---|\n| `AvValidationError` | `A2004`, `A2007`, `A2009` | The arguments break a rule, a transaction `$ref` is wrong, or a limit is crossed. |\n| `AvNotFoundError` | `A2003` | A `unique` operation matched no row. |\n| `AvConflictError` | `A2008`, `A2013`, `A2014` | A duplicate or a missing parent (`A2008`), or the row changed under the write (`A2013`, `A2014`: read again and retry). |\n| `AvAuthError` | `A4000`, `A4001`, `A4002` | Sign in first (`A4000`), or not allowed (`A4001`, `A4002`). |\n| `AvContractMismatchError` | `A2005` | The client is stale: regenerate it (`reference/client-setup.md`). |\n| `AvProtocolError` | `A2000`, `A2001`, `A2002`, `A2006`, `A2010`, `A2011`, `A2012` | A request the server cannot read or route. |\n| `AvInternalError` | `A3000`, `A3001`, `A3002`, `A3003`, `A3004` | The server failed; the real error is in its log. |\n| `AvTransportError` | none | No Aventara answer arrived: the network failed, or a proxy answered with its own page. `error.status` is the HTTP status, or `null`. |\n\n`AvError` is the base class of every class in the table but `AvTransportError`. A `$access` call (Aventara Access) may also throw `AvAccessError`, which is not an `AvError`.\n\nAn `AvValidationError` lists each problem in `error.cause.issues`: `{ path, code, message }`, plus `check` and `params` for a failed field validator (`check: \"max_length\"`, `params: { maximum: 80 }`). `path` is where in the call (`[\"arguments\", \"data\", \"email\"]`), so a form can show `message` beside the field `path` ends with. The client runs the server's field validators before sending: a value they refuse throws this same error, and nothing is sent.\n\n```ts\nimport avClient, { AvValidationError } from \"./api/AvClient\";\n\ntry {\n await avClient.User.create.one({ data: { email: \"not-an-email\" } });\n} catch (error) {\n if (!(error instanceof AvValidationError)) throw error;\n for (const issue of error.cause.issues ?? []) console.log(issue.path?.at(-1), issue.message);\n}\n```\n\n## Calls made together share one request\n\nCalls made in the same tick (before the microtask queue settles) go out as one `POST <api>/_all`, an Operation Set. Each call still resolves or throws on its own. Write nothing for it: no batching endpoint, no loader.\n\n```ts\nimport avClient from \"./api/AvClient\";\n\nconst [posts, published] = await Promise.all([\n avClient.Post.find.many({ where: { published: true }, select: [\"id\", \"title\"] }),\n avClient.Post.find.count({ where: { published: true } }),\n]); // one POST /api/_all\n```\n\n- Each call runs exactly as alone: validation, exposure, policy, limits and pipelines are those of its own route, and so is its answer.\n- Not atomic and in no order: the calls run concurrently, and a write that ran stays written whatever the others answered. Writes that must hold together, or a call that needs another's result, belong in a transaction (`avClient.transaction([avClient.tx.Post.create.one(…), …])`, `POST /_transactions`).\n- Reads and writes mix. A group larger than `limits.maxAll` (10) or `limits.maxRequestBytes` is split by the client.\n- Always sent alone: a call with `headers` or `requestId` in its options, a call or client with `grouping: false`, a transaction, and a cached read (a `GET`). Headers every call needs (a token) go in `onCall`, set once, `new AvClient({ onCall })`, so the calls still group (see \"Hooks\" below); calls whose `onCall` additions differ never share a request.\n- A server older than rc.11 answers `/_all` with `A2002`; the client resends the calls one by one and stops grouping.\n\n## Transactions\n\nWrites that must commit together, or a step that uses an earlier step's result, go in one transaction:\n\n```ts\nimport avClient from \"./api/AvClient\";\n\nconst author = avClient.tx.User.create.one({ data: { email: \"ada@example.com\" }, select: [\"id\"] });\nconst post = avClient.tx.Post.create.one({ data: { title: \"Hello\", authorId: author.$ref(\"id\") }, select: [\"id\"] });\nconst [user, created] = await avClient.transaction([author, post]); // one result per step, typed, in order\n```\n\n- `avClient.tx.<Resource>.<family>.<variant>(args)` describes a step and sends nothing. `avClient.transaction([...])` sends the steps once, as one `POST <api>/_transactions`; they all commit or none does.\n- `step.$ref(\"field\")` stands for that field of an earlier step's result, on a step whose result is one row (`create.one`, `find.unique`, `update.unique`, …). The field must be in that step's result: put it in its `select` (a field the result does not carry is refused, `A2007` / `V1010`). Write it in `data` at any depth (a field, a `$connect`), in a `where` or in a `cursor`, never in a projection's nested `where` or `orderBy`. Its type must fit where it is written; a mismatch is a type error.\n- A failing step rolls every step back and throws its error, with `error.cause.operation` the failing step's index.\n- The client has no `tx` when the server's client layer sets `transactionsStyle: \"server\"` or `\"none\"`.\n- Server code: `framework.transaction([framework.appTx.…])`, the same `$ref`s. In Next.js, `avApp.appTx` with `avApp.transaction([...])`, or `avClient.tx` with `avClient.transaction([...])` from `@/aventara/server`, never mixed in one list.\n\n## Hooks: `onCall`, `onSuccess`, `onError`\n\nThree `AvClient` options see every call: operations, cached reads, a transaction (once), `$access` calls. `onCall` adds to the request; `onSuccess` and `onError` only watch.\n\n```ts\nimport { AvAuthError, AvClient } from \"./api/AvClient\";\n\nexport const authed = new AvClient({\n onCall: () => ({ headers: { Authorization: `Bearer ${localStorage.getItem(\"token\") ?? \"\"}` }, credentials: \"include\" }),\n onError: (_call, error) => {\n if (error instanceof AvAuthError) location.assign(\"/login\");\n },\n});\n```\n\n- `onCall(call)` runs when the call is made, before grouping, and may return or resolve to `{ headers, credentials, cache, keepalive, priority, referrerPolicy }`. Calls with equal additions share one `/_all`. An async one holds back only its own call: it is sent when its `onCall` settles, with the calls queued then, so calls group when their `onCall` settles together with equal additions. An `onCall` doing its own I/O per call (reading the token again each time) sends each call alone; to keep grouping, await one shared in-flight promise, cleared once it settles: `let inFlight; const token = () => (inFlight ??= readToken().finally(() => { inFlight = undefined; }));`. If `token()` rejects, the call is sent without `Authorization` (the hook warning fires) and will likely reach `onError` as a refusal. A hanging `onCall` stalls only its own call, and an abort settles that call at once. A call made inside a hook never waits on the call that ran it.\n- Order: `onCall`'s headers, then the call's own `headers` option (wins), then the client's own (always win). `Content-Type`, `Accept` and the `Aventara-*` headers (canonical or lower-case), and `method`, `body`, `signal`, `redirect`, `mode`, are type errors in `onCall`; at runtime those headers in any case, invalid names and values with a line break, a control character, or a character outside Latin-1 are dropped with a warning (`AVENTARA_CLIENT_HOOK_HEADER_DROPPED`), once per header name for each `onCall`.\n- `call` is `{ kind, label, resource, family, variant, requestId? }` for an operation (`kind: \"transaction\"` or `\"access\"` with `verb` otherwise); never the arguments. To type a hook declared on its own, import `AvCall` and `AvCallAdditions` from the generated client.\n- `onSuccess(call, result)` and `onError(call, error)` are sinks returning `void` (any function fits, async too): side effects only, what they return is ignored and never awaited, so they never delay the caller, and they cannot swallow, change or retry a call. `onError` sees refusals, validation errors, transport failures (`AvTransportError`) and aborts, grouped or alone.\n- A hook that throws or rejects is reported once as a warning (`AVENTARA_CLIENT_HOOK_THREW`) and skipped; the call goes on, with no additions when it was `onCall`.\n- `fetch` only swaps the transport (tests, another runtime's fetch): it must return the `Response` unread. Do not put headers or a token in it. An entrypoint URL carrying credentials (`https://user:pass@…`) is refused by `avclient`: add them through `onCall`.\n- Next.js: put the hooks in `src/aventara/client-options.ts` (or `.js`; no other extension) as `export default { onCall, onSuccess, onError } satisfies Pick<AvClientOptions, \"onCall\" | \"onSuccess\" | \"onError\">` (type from `@/aventara/client`), then run `aventara:prepare` (it runs `aventara-build --client`). Both `@/aventara/client` and `@/aventara/universal` use them, reading the default export when a hook runs, so the file may value-import from `@/aventara/client` (an error class); `fetch`, `grouping` and `entrypoint` there are type errors. A file with no default export leaves the client without hooks, with one warning (`AVENTARA_CLIENT_OPTIONS_NO_DEFAULT`). The file is bundled into the browser: no secrets or server-only imports; branch on `typeof window`. On the server, onCall's headers reach the in-process request but not `ctx.$state`, which comes from the request being served. `@/aventara/server` is not an AvClient and takes no hooks.\n\n## Calls run when you make them\n\nA call is checked, encoded and queued when it is made, as `fetch` is: a call never awaited still runs, and awaiting it twice sends it once. To describe a call now and make it later, wrap it in `avLazy`:\n\n```ts\nimport avClient, { avLazy } from \"./api/AvClient\";\n\nconst drafts = avLazy(() => avClient.Post.find.many({ where: { published: false }, select: [\"id\"] })); // nothing sent\nconst list = await drafts; // sent now\n```\n\n| Where `avLazy` comes from | Awaited again |\n|---|---|\n| The generated client (browser) | Keeps its one answer. |\n| `@aventara/core` (server code) | Calls again each time; it keeps no answer, so never hold one in module scope expecting a cache. |\n| Next.js `@/aventara/universal` on the server | Keeps its answer within one request's React render only. |\n\n## `avClient.all`\n\nAn object of calls in, one typed value per key out. The keys are yours and never sent.\n\n```ts\nimport avClient, { AvNotFoundError } from \"./api/AvClient\";\n\n// Strict (default): the first failure rejects `all` with that error and aborts its other calls.\nconst { list, total } = await avClient.all({\n list: avClient.Post.find.many({ select: [\"id\", \"title\"] }),\n total: avClient.Post.find.count({}),\n});\n\n// settle: true: every key settles on its own; `all` never rejects for one key.\nconst panels = await avClient.all(\n {\n posts: avClient.Post.find.many({ select: [\"id\", \"title\"] }),\n me: avClient.User.find.unique({ where: { email: \"ada@example.com\" }, select: [\"id\", \"email\"] }),\n },\n { settle: true },\n);\nif (panels.me.status === \"rejected\") {\n if (panels.me.reason instanceof AvNotFoundError) console.log(\"no such user\");\n} else {\n console.log(panels.me.value.email, list.length, total);\n}\n```\n\nStrict is fail-fast, not atomic: a write that already ran stays written. Use strict when the page needs every value, `settle: true` when each panel renders or fails alone. Every entry must be a call (or an `avLazy`); a plain value does not compile.\n\n## Aborting\n\nEach call has its own abort: `{ signal }` in a call's options aborts that call only; `avClient.all({ … }, { signal })` aborts every call of that `all`, and a call inside it stops on whichever signal fires first.\n\n```ts\nimport avClient from \"./api/AvClient\";\n\nconst page = new AbortController();\nconst search = new AbortController();\n\nconst result = avClient.all(\n {\n hits: avClient.Post.find.many({ where: { title: { contains: \"rc\" } } }, { signal: search.signal }),\n count: avClient.Post.find.count({}),\n },\n { signal: page.signal },\n);\nsearch.abort(); // only `hits` stops; strict `all` then rejects with its AbortError\n```\n\n- Aborted before the request is sent: the call is dropped. After: it rejects at once; the other calls of the same request go on.\n- Calls grouped into the same request but not in that `all` are never touched. The HTTP request itself is aborted only when every call in it is.\n- A started database query still finishes on the server (no cancel); a started write is written.\n- Nothing aborts for you: no default timeout, no abort on navigation. Pass the signal your data library gives (TanStack Query's `queryFn` receives one: `queryFn: ({ signal }) => avClient.Post.find.many({}, { signal })`), or abort in an effect's cleanup.\n\n## Turning grouping off\n\n```ts\nimport avClient, { AvClient } from \"./api/AvClient\";\n\nawait avClient.Post.find.many({ select: [\"id\"] }, { grouping: false }); // this call, on its own route\nconst plain = new AvClient({ grouping: false }); // every call of this client\nawait plain.Post.find.count({});\n```\n\nOnly for a reason: debugging in the network tab, per-route monitoring or rate limits, or a proxy that lets through `/_resources/…` but not `/_all`. A transaction takes no `grouping` option.\n\n## On the server\n\nServer code has the same `all`: `framework.serverClient.all` (NestJS, any host) and `avClient.all` from `@/aventara/server` (Next.js). `framework.application` and `avApp` have no `all`.\n\n```tsx\n// src/app/dashboard/page.tsx\nimport { avLazy } from \"@aventara/core\";\nimport { avClient } from \"@/aventara/server\";\n\nexport default async function Dashboard() {\n const { posts, published } = await avClient.all({\n posts: avClient.Post.find.many({ select: [\"id\", \"title\"] }),\n published: avLazy(() => avClient.Post.find.count({ where: { published: true } })),\n });\n return <p>{posts.length} posts, {published} published</p>;\n}\n```\n\n- Nothing is grouped and nothing goes over HTTP: each call runs in process, in the request that made it, with that request's state and principal (`framework.serverClient` calls take `{ state }` like any server call).\n- Any number of entries: `limits.maxAll` guards `POST /_all` only.\n- A plain call has already started when made, so `all` can only await it. An `avLazy` entry is started by `all`: at most `limits.maxAllConcurrency` (4) at once, in key order, and none after a strict `all` failed or was aborted.\n- Next.js `@/aventara/universal` groups in the browser only; on the server each of its calls runs on its own.\n\n## The two limits\n\n| Limit | Default | Bounds |\n|---|---|---|\n| `limits.maxAll` | `10` | Operations in one `POST /_all`. More is refused whole: `422 A2009` / `V1014`. The generated client splits for you. |\n| `limits.maxAllConcurrency` | `4` | Operations of one set (HTTP or in process) running at once; the rest wait, in key order. At least `1`. |\n\nOrdinary limits: set them at the root, override them in `client` or `application` (`limits: { maxAll: 20 }`). Do not grow the database pool for `/_all`; the concurrency limit keeps one request to its share.\n\n## Cached reads from the client\n\nA read the server caches (its Resource's `cache` stage) is sent by the generated client as `GET <api>/_resources/<R>/find/<variant>?q=<args>&v=<contract hash>`, alone, never in an Operation Set. Leave `requestId` off a cached cross-origin read (it adds a CORS preflight). A URL over 4096 characters goes as a `POST` and is not cached. The `POST` route stays for any client.\n\n## By hand (curl, a custom client)\n\n`POST <api>/_all` takes the two identity headers and `{ \"operations\": { \"<key>\": { \"resource\": \"Post\", \"family\": \"find\", \"variant\": \"many\", \"args\": { … } } } }` (keys are identifiers; each entry is exactly those four members). It answers `200 A1000` with `data` holding each key's own envelope (`{ data, code, cause }`): read each key's `code`, not the status. With `Accept: application/x-ndjson` it streams one line per entry as it finishes, `{\"key\":\"…\",\"envelope\":{…}}`; the generated client does this for you, so write it only in a client of your own. Too many entries: `422 A2009`; none: `422 A2004`.\n\nA plan of your own, `POST <api>/_transactions` with `{ \"operations\": [ … ] }`, needs each step's `fingerprint`: compute it with `await AvProtocol.fingerprint({ resource, family, variant, args })` from `@aventara/core/protocol` over the step's wire-form `args`, the function the server checks it with. It is async: a step whose `fingerprint` is the unawaited promise serializes as `{}`, and a wrong fingerprint answers `422 A2007` / `V1017`. A layer with `transactionsStyle: \"server\"` or `\"none\"` has no such route (`404 A2002`).\n\n## Keeping this file current\n\nGenerated by `avclient` {{CLIENT_VERSION}}. After upgrading `@aventara/client`, refresh it: run `avclient skill` through your package manager. A file you edited is kept, with a warning naming it, and stops receiving fixes (pass `--force` to replace it).\n",
|
|
3
3
|
"reference/client-setup.md": "# Setting up and regenerating the client\n\nA separate frontend calls the API through a client generated from the server's client contract. In a Next.js app with Aventara inside it, `aventara:prepare` writes the browser client (`@/aventara/client`) instead, and nothing on this page applies.\n\n## `framework.client.ts`\n\n`avclient init` writes it in the frontend's root, with the `avclient:generate` script and `@aventara/client` as a devDependency:\n\n```ts\n// framework.client.ts\nimport { defineClientConfig, env } from \"@aventara/client\";\n\nexport default defineClientConfig({\n entrypoint: env(\"AVENTARA_API_URL\"),\n generateAt: \"./src/api\",\n});\n```\n\n- `entrypoint` is the server's URL with its `api` path (`http://localhost:3000/api`), with no credentials in it. `env(\"NAME\")` reads the variable from the environment, then `.env.<mode>.local`, `.env.<mode>`, `.env.local` and `.env` (`mode` is `NODE_ENV`, else `development`). `avclient init` writes `AVENTARA_API_URL` into `.env`.\n- The generated client calls the `entrypoint` it was generated with. To build a client for staging or production, set `AVENTARA_API_URL` to that server and regenerate. `new AvClient({ entrypoint })` points one client at another deployment that serves the same contract (a test server, SSR).\n- `generateAt` is where the client goes: `avclient generate` owns `AvClient.ts` and `generated/` in it and touches nothing else there.\n- `tsconfigFile` names the tsconfig that compiles the client, only when it is not the nearest `tsconfig.json` above `generateAt` (Vite's `tsconfig.app.json`).\n- `accessOrigins`, only with Aventara Access on another origin than the entrypoint's: list that origin (`[\"https://auth.example.com\"]`). Without it `avclient generate` refuses with an error naming that origin and `accessOrigins`, because sign-in credentials would go there.\n\n## Regenerate after the server changes\n\nRun the frontend's `avclient:generate` script (it runs `avclient generate`) while the server runs: it reads the client contract from `entrypoint`. Regenerate after any change to the server's client contract: the schema, exposure, field validators, limits, operations, the client layer's `transactionsStyle`, a Json field's shape, a `cache` stage, or which fields compute fills.\n\nA client generated before such a change is refused on its next call with `409 A2005`, thrown as `AvContractMismatchError`. The fix is always the same: regenerate against the server the client calls. Never edit the generated files to make a call fit.\n\n- A failed run leaves the previous client as it was.\n- `avclient generate` lists anything in `AvClient.ts` or `generated/` it did not write and asks before replacing it; `--yes` answers yes. Where nobody can answer (stdin is not a terminal, as in CI), it refuses and changes nothing: pass `--yes` there.\n- Commit the generated client. Regenerating it needs a running server, so CI and teammates build from the committed copy.\n- The same contract always gives the same bytes.\n"
|
|
4
4
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aventara/client",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.14",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"description": "Development-time generator for Aventara typed remote clients.",
|
|
6
6
|
"type": "module",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"CHANGELOG.md"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@aventara/core": "1.0.0-rc.
|
|
31
|
+
"@aventara/core": "1.0.0-rc.14",
|
|
32
32
|
"c12": "3.3.4",
|
|
33
33
|
"get-tsconfig": "4.10.0"
|
|
34
34
|
},
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@aventara/testing": "1.0.0-rc.
|
|
47
|
+
"@aventara/testing": "1.0.0-rc.14",
|
|
48
48
|
"@types/node": "24.10.1",
|
|
49
49
|
"typescript": "^5.9.2"
|
|
50
50
|
},
|