@aventara/client 1.0.0-rc.1 → 1.0.0-rc.10

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +194 -0
  2. package/README.md +37 -1
  3. package/dist/cli/command.parser.d.ts +4 -3
  4. package/dist/cli/command.parser.js +3 -3
  5. package/dist/cli/generation-failure.renderer.d.ts +2 -1
  6. package/dist/cli/generation-success.renderer.d.ts +2 -1
  7. package/dist/cli/step.reporter.d.ts +2 -1
  8. package/dist/cli/terminal.prompter.d.ts +2 -1
  9. package/dist/config/client-config.interface.d.ts +17 -0
  10. package/dist/config/config.loader.d.ts +8 -3
  11. package/dist/config/config.loader.js +20 -2
  12. package/dist/config/config.resolver.d.ts +10 -3
  13. package/dist/config/config.resolver.js +28 -0
  14. package/dist/config/env.cascade.d.ts +2 -6
  15. package/dist/config/env.cascade.js +3 -3
  16. package/dist/config/module-style.resolver.d.ts +3 -6
  17. package/dist/config/module-style.resolver.js +2 -2
  18. package/dist/config/tsconfig.locator.d.ts +2 -1
  19. package/dist/config/tsconfig.locator.js +14 -2
  20. package/dist/contract/contract.acceptance.d.ts +0 -1
  21. package/dist/contract/contract.acceptance.js +1 -1
  22. package/dist/contract/contract.fetcher.d.ts +2 -1
  23. package/dist/contract/contract.loader.d.ts +3 -2
  24. package/dist/emit/access.emitter.d.ts +12 -0
  25. package/dist/emit/access.emitter.js +263 -0
  26. package/dist/emit/banner.emitter.d.ts +6 -1
  27. package/dist/emit/banner.emitter.js +12 -8
  28. package/dist/emit/client-surface.emitter.js +79 -7
  29. package/dist/emit/client-tree.emitter.d.ts +9 -2
  30. package/dist/emit/client-tree.emitter.js +45 -14
  31. package/dist/emit/derivation.emitter.d.ts +3 -3
  32. package/dist/emit/derivation.emitter.js +6 -3
  33. package/dist/emit/emitted-tree.interface.d.ts +1 -1
  34. package/dist/emit/field-check.emitter.d.ts +2 -1
  35. package/dist/emit/field-check.emitter.js +2 -13
  36. package/dist/emit/module-specifier.scanner.d.ts +2 -1
  37. package/dist/emit/name.deriver.d.ts +2 -3
  38. package/dist/emit/name.deriver.js +4 -1
  39. package/dist/emit/named-type.emitter.js +12 -2
  40. package/dist/emit/runtime.emitter.d.ts +5 -4
  41. package/dist/emit/runtime.emitter.js +101 -7
  42. package/dist/emit/scalar.codec.js +223 -6
  43. package/dist/emit/transaction.emitter.js +7 -3
  44. package/dist/generate.d.ts +3 -2
  45. package/dist/generate.js +1 -1
  46. package/dist/hydration/hydration-tag.format.d.ts +11 -0
  47. package/dist/hydration/hydration-tag.format.js +6 -0
  48. package/dist/init/client-init.planner.d.ts +3 -2
  49. package/dist/init/client-init.questions.d.ts +1 -12
  50. package/dist/init/client-init.questions.js +2 -2
  51. package/dist/internal.d.ts +25 -0
  52. package/dist/internal.js +11 -0
  53. package/dist/output/output.validator.d.ts +3 -4
  54. package/dist/output/output.validator.js +2 -2
  55. package/dist/output/output.writer.d.ts +3 -2
  56. package/package.json +9 -5
@@ -4,7 +4,7 @@ export interface LocatedTsconfig {
4
4
  readonly path: string;
5
5
  readonly config: TsConfigJsonResolved;
6
6
  }
7
- export interface TsconfigLocation {
7
+ interface TsconfigLocation {
8
8
  /** The absolute `generateAt`: the search starts there. */
9
9
  readonly generateAt: string;
10
10
  /** The generated entry file the project must include, absolute. */
@@ -19,3 +19,4 @@ export interface TsconfigLocation {
19
19
  * @throws AvClientConfigError when the named `tsconfigFile` is missing or unreadable.
20
20
  */
21
21
  export declare function locateTsconfig(location: TsconfigLocation): LocatedTsconfig | undefined;
22
+ export {};
@@ -11,18 +11,30 @@ export function locateTsconfig(location) {
11
11
  return undefined;
12
12
  }
13
13
  if (found.config.references === undefined ||
14
- createFilesMatcher(found)(location.entryFile) !== undefined) {
14
+ includesEntry(found, location.entryFile)) {
15
15
  return found;
16
16
  }
17
17
  for (const reference of found.config.references) {
18
18
  const referenced = referencedTsconfig(found.path, reference.path);
19
19
  if (referenced !== undefined &&
20
- createFilesMatcher(referenced)(location.entryFile) !== undefined) {
20
+ includesEntry(referenced, location.entryFile)) {
21
21
  return referenced;
22
22
  }
23
23
  }
24
24
  return found;
25
25
  }
26
+ function includesEntry(tsconfig, file) {
27
+ const directory = path.dirname(tsconfig.path);
28
+ const local = (entries) => entries.map((entry) => path.isAbsolute(entry) ? path.relative(directory, entry) || "." : entry);
29
+ const { files, include, exclude } = tsconfig.config;
30
+ const config = {
31
+ ...tsconfig.config,
32
+ ...(files === undefined ? {} : { files: local(files) }),
33
+ ...(include === undefined ? {} : { include: local(include) }),
34
+ ...(exclude === undefined ? {} : { exclude: local(exclude) }),
35
+ };
36
+ return (createFilesMatcher({ path: tsconfig.path, config })(file) !== undefined);
37
+ }
26
38
  function namedTsconfig(file) {
27
39
  try {
28
40
  readFileSync(file, "utf8");
@@ -1,5 +1,4 @@
1
1
  import type { AvContractClient } from "@aventara/core";
2
- export declare const SUPPORTED_PROTOCOL_VERSIONS: ReadonlySet<number>;
3
2
  export type ContractRejectionReason = "protocol-unsupported" | "hash-mismatch" | "structure-invalid";
4
3
  export type ContractAccepted = {
5
4
  readonly accepted: true;
@@ -1,5 +1,5 @@
1
1
  import { AVENTARA_PROTOCOL_VERSION, AvCanonicalJsonError, computeClientContractHash, validateClientContractStructure, } from "@aventara/core/internal";
2
- export const SUPPORTED_PROTOCOL_VERSIONS = new Set([
2
+ const SUPPORTED_PROTOCOL_VERSIONS = new Set([
3
3
  AVENTARA_PROTOCOL_VERSION,
4
4
  ]);
5
5
  const REFUSED_EXIT_CODE = 1;
@@ -7,7 +7,7 @@ export type ContractFetch = (url: URL, init: {
7
7
  readonly headers?: Readonly<Record<string, string>>;
8
8
  }) => Promise<ContractResponse>;
9
9
  export declare const CONTRACT_NOT_MODIFIED: unique symbol;
10
- export type ContractTransportFailureReason =
10
+ type ContractTransportFailureReason =
11
11
  /** No response at all: DNS, refused connection, TLS, abort. */
12
12
  "request-failed"
13
13
  /** A response that is not 2xx — nothing was served for the protocol to judge. */
@@ -28,3 +28,4 @@ export declare function displayUrl(url: URL): string;
28
28
  * @throws AvContractTransportError when no JSON value arrives.
29
29
  */
30
30
  export declare function fetchClientContractBody(entrypoint: ClientEntrypoint, fetch?: ContractFetch, ifNoneMatch?: string): Promise<unknown>;
31
+ export {};
@@ -1,7 +1,7 @@
1
1
  import type { AvContractClient } from "@aventara/core";
2
2
  import type { ClientEntrypoint } from "../config/client-config.interface.js";
3
3
  import { type ContractFetch } from "./contract.fetcher.js";
4
- export interface ClientContractLoadInput {
4
+ interface ClientContractLoadInput {
5
5
  readonly entrypoint: ClientEntrypoint;
6
6
  readonly fetch?: ContractFetch;
7
7
  }
@@ -10,7 +10,7 @@ export interface ClientContractLoadInput {
10
10
  * @throws AvContractProtocolError when what arrived is refused.
11
11
  */
12
12
  export declare function loadClientContract(input: ClientContractLoadInput): Promise<AvContractClient>;
13
- export interface ClientContractSince {
13
+ interface ClientContractSince {
14
14
  readonly contract: AvContractClient;
15
15
  /** The deployment answered `304`: `contract` is the one the caller held. */
16
16
  readonly notModified: boolean;
@@ -22,3 +22,4 @@ export interface ClientContractSince {
22
22
  * `stored` the GET is unconditional.
23
23
  */
24
24
  export declare function loadClientContractSince(input: ClientContractLoadInput, stored: AvContractClient | undefined): Promise<ClientContractSince>;
25
+ export {};
@@ -0,0 +1,12 @@
1
+ import type { AvContractClient } from "@aventara/core";
2
+ import { ACCESS_VERB_NAMES, ACCESS_VERBS } from "@aventara/core/internal";
3
+ import type { EmittedModule } from "./emitted-tree.interface.js";
4
+ import { type ClientModuleStyle } from "./module-style.interface.js";
5
+ /** One verb `$access` offers beside `can`. */
6
+ export type AccessVerbName = (typeof ACCESS_VERB_NAMES)[number];
7
+ /** Whether a verb takes arguments, and what it resolves (core's verb table). */
8
+ export declare function accessVerbShape(name: AccessVerbName): (typeof ACCESS_VERBS)[AccessVerbName];
9
+ /** The verbs the section states, in `$access`'s order; none without `verbs`. */
10
+ export declare function accessVerbsOf(contract: AvContractClient): readonly AccessVerbName[];
11
+ /** `runtime/access.ts`, or nothing when the section states no verb. */
12
+ export declare function emitAccessModules(contract: AvContractClient, style: ClientModuleStyle): readonly EmittedModule[];
@@ -0,0 +1,263 @@
1
+ import { ACCESS_VERB_NAMES, ACCESS_VERBS } from "@aventara/core/internal";
2
+ import { importSpecifier, } from "./module-style.interface.js";
3
+ export function accessVerbShape(name) {
4
+ return ACCESS_VERBS[name];
5
+ }
6
+ export function accessVerbsOf(contract) {
7
+ const verbs = contract.access?.verbs;
8
+ return verbs === undefined
9
+ ? []
10
+ : ACCESS_VERB_NAMES.filter((name) => verbs[name] !== undefined);
11
+ }
12
+ export function emitAccessModules(contract, style) {
13
+ const verbs = contract.access?.verbs;
14
+ const names = accessVerbsOf(contract);
15
+ if (verbs === undefined || names.length === 0) {
16
+ return [];
17
+ }
18
+ const from = (module) => JSON.stringify(importSpecifier(module, style));
19
+ const table = names
20
+ .map((name) => {
21
+ const verb = verbs[name];
22
+ return `\t${name}: { method: ${JSON.stringify(verb?.method)}, path: ${JSON.stringify(verb?.path)}, result: ${JSON.stringify(verb?.result)} },\n`;
23
+ })
24
+ .join("");
25
+ return [
26
+ {
27
+ path: "runtime/access.ts",
28
+ source: `import { AvTransportError } from ${from("./errors")};
29
+ import type { AvCallOptions, PlatformSignal, TransportConnection } from ${from("./transport")};
30
+
31
+ /**
32
+ * Aventara Access's verbs, as this deployment's ClientContract states them:
33
+ * each one a request to Access's own route, its result projected onto the
34
+ * schema the ClientContract promises.
35
+ */
36
+
37
+ /**
38
+ * Aventara Access answered a verb with a status that is not 2xx. It carries the
39
+ * status and Access's own code and message; it is no AvError, because Access's
40
+ * answers are not framework envelopes.
41
+ */
42
+ export class AvAccessError extends Error {
43
+ override readonly name: string = "AvAccessError";
44
+ /** The HTTP status Access answered. */
45
+ readonly status: number;
46
+ /** Access's own error code, or null when its answer named none. */
47
+ readonly code: string | null;
48
+
49
+ constructor(message: string, status: number, code: string | null) {
50
+ super(message);
51
+ this.status = status;
52
+ this.code = code;
53
+ }
54
+ }
55
+
56
+ /** Where the app mounted Aventara Access. */
57
+ const ACCESS_URL = ${JSON.stringify(contract.access?.url ?? "")};
58
+
59
+ /** Each verb this client offers: its route under ACCESS_URL and its result's schema. */
60
+ const VERBS = {
61
+ ${table}} as const;
62
+
63
+ /** One verb this client offers. */
64
+ export type AccessVerb = keyof typeof VERBS;
65
+
66
+ /** A schema node, as the projection reads it. */
67
+ type Schema = { readonly [key: string]: unknown };
68
+
69
+ /** What a verb hands fetch. */
70
+ interface AccessInit {
71
+ readonly method: "GET" | "POST";
72
+ readonly headers: Readonly<Record<string, string>>;
73
+ readonly body?: string;
74
+ readonly cache?: "no-store";
75
+ readonly signal?: PlatformSignal;
76
+ }
77
+
78
+ /** What a verb reads of fetch's answer. */
79
+ interface AccessAnswer {
80
+ readonly status: number;
81
+ text(): Promise<string>;
82
+ }
83
+
84
+ /** The URL of a verb's route: a root-relative ACCESS_URL put under an absolute entrypoint's origin. */
85
+ function routeOf(entrypoint: string, path: string): string {
86
+ const base = ACCESS_URL.replace(/\\/+$/, "");
87
+ const origin = /^[a-z][a-z0-9+.-]*:\\/\\/[^/?#]*/i.exec(entrypoint)?.[0];
88
+ return base.startsWith("/") && origin !== undefined ? origin + base + path : base + path;
89
+ }
90
+
91
+ function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
92
+ return typeof value === "object" && value !== null && !Array.isArray(value);
93
+ }
94
+
95
+ /**
96
+ * \`schema\` as a member's schema.
97
+ *
98
+ * @throws TypeError when it is none (a free-form object or list, a tuple read
99
+ * past its length): outside the forms a ClientContract states.
100
+ */
101
+ function memberSchema(schema: unknown): Schema {
102
+ if (!isRecord(schema)) {
103
+ throw new TypeError("the schema is not one of the forms a ClientContract states");
104
+ }
105
+ return schema as Schema;
106
+ }
107
+
108
+ /** Whether \`value\` can be read by \`schema\` at all: its JSON type. */
109
+ function fits(value: unknown, schema: Schema): boolean {
110
+ if (Object.hasOwn(schema, "const")) {
111
+ return value === schema["const"];
112
+ }
113
+ if (Array.isArray(schema["enum"])) {
114
+ return schema["enum"].includes(value);
115
+ }
116
+ for (const keyword of ["anyOf", "oneOf"]) {
117
+ const options = schema[keyword];
118
+ if (Array.isArray(options)) {
119
+ return options.some((option) => fits(value, option as Schema));
120
+ }
121
+ }
122
+ switch (schema["type"]) {
123
+ case "null":
124
+ return value === null;
125
+ case "boolean":
126
+ return typeof value === "boolean";
127
+ case "string":
128
+ return typeof value === "string";
129
+ case "number":
130
+ return typeof value === "number";
131
+ case "integer":
132
+ return Number.isInteger(value);
133
+ case "array":
134
+ return Array.isArray(value);
135
+ case "object":
136
+ return isRecord(value);
137
+ default:
138
+ return false;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * \`value\` projected onto \`schema\`: every object down to the members the schema
144
+ * declares, a list member by member. The schema is the promise.
145
+ *
146
+ * @throws TypeError when the value is not one the schema describes.
147
+ */
148
+ function projected(value: unknown, schema: Schema): unknown {
149
+ if (!fits(value, schema)) {
150
+ throw new TypeError("the value is not the one the ClientContract describes");
151
+ }
152
+ for (const keyword of ["anyOf", "oneOf"]) {
153
+ const options = schema[keyword];
154
+ if (Array.isArray(options)) {
155
+ const option = options.find((candidate) => fits(value, candidate as Schema)) as Schema;
156
+ return projected(value, option);
157
+ }
158
+ }
159
+ if (Array.isArray(value)) {
160
+ const prefix = schema["prefixItems"];
161
+ const items = Array.isArray(prefix) ? undefined : memberSchema(schema["items"]);
162
+ return Object.freeze(
163
+ value.map((member: unknown, index: number) =>
164
+ projected(member, items ?? memberSchema((prefix as readonly unknown[])[index])),
165
+ ),
166
+ );
167
+ }
168
+ if (isRecord(value)) {
169
+ const properties = schema["properties"];
170
+ if (isRecord(properties)) {
171
+ const copy: Record<string, unknown> = {};
172
+ for (const [key, member] of Object.entries(properties)) {
173
+ if (Object.hasOwn(value, key) && value[key] !== undefined) {
174
+ copy[key] = projected(value[key], member as Schema);
175
+ }
176
+ }
177
+ return Object.freeze(copy);
178
+ }
179
+ const values = memberSchema(schema["additionalProperties"]);
180
+ return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, member]) => [key, projected(member, values)])));
181
+ }
182
+ return value;
183
+ }
184
+
185
+ /** Access's error answer, read for its code and message. */
186
+ function refusalOf(verb: AccessVerb, status: number, text: string): AvAccessError {
187
+ let body: unknown;
188
+ try {
189
+ body = JSON.parse(text);
190
+ } catch {
191
+ body = undefined;
192
+ }
193
+ const code = isRecord(body) && typeof body["code"] === "string" ? body["code"] : null;
194
+ const message =
195
+ isRecord(body) && typeof body["message"] === "string" ? body["message"] : "Aventara Access refused $access." + verb + " (HTTP " + status + ").";
196
+ return new AvAccessError(message, status, code);
197
+ }
198
+
199
+ /**
200
+ * Runs one Access verb: one request to its route, its 2xx answer projected onto
201
+ * its result schema (\`null\` stays \`null\`: no one is signed in).
202
+ *
203
+ * @throws AvAccessError on a non-2xx answer; AvTransportError when no answer
204
+ * arrived, or the answer is not the value the ClientContract describes. A
205
+ * caller's abort rejects with what fetch rejected with.
206
+ */
207
+ export async function callAccess(
208
+ connection: TransportConnection,
209
+ verb: AccessVerb,
210
+ args: unknown,
211
+ options: AvCallOptions = {},
212
+ ): Promise<unknown> {
213
+ const { method, path, result } = VERBS[verb];
214
+ const operation = "$access." + verb;
215
+ const headers: Record<string, string> = { ...options.headers };
216
+ let init: AccessInit;
217
+ if (method === "GET") {
218
+ init = { method, headers, cache: "no-store" };
219
+ } else {
220
+ headers["Content-Type"] = "application/json";
221
+ init = { method, headers, body: JSON.stringify(args ?? {}) };
222
+ }
223
+ if (options.signal !== undefined) {
224
+ init = { ...init, signal: options.signal };
225
+ }
226
+ const fetch = connection.fetch as unknown as (url: string, init: AccessInit) => Promise<AccessAnswer>;
227
+ let answer: AccessAnswer;
228
+ let text: string;
229
+ try {
230
+ answer = await fetch(routeOf(connection.entrypoint, path), init);
231
+ text = await answer.text();
232
+ } catch (error) {
233
+ if (options.signal?.aborted === true) {
234
+ throw error;
235
+ }
236
+ throw new AvTransportError("The request for " + operation + " failed before any answer arrived.", null);
237
+ }
238
+ if (answer.status < 200 || answer.status > 299) {
239
+ throw refusalOf(verb, answer.status, text);
240
+ }
241
+ let value: unknown;
242
+ try {
243
+ value = text.trim() === "" ? null : JSON.parse(text);
244
+ } catch {
245
+ throw new AvTransportError("The answer to " + operation + " (HTTP " + answer.status + ") is not JSON.", answer.status);
246
+ }
247
+ if (value === null) {
248
+ return null;
249
+ }
250
+ try {
251
+ return projected(value, result as Schema);
252
+ } catch (error) {
253
+ throw new AvTransportError(
254
+ "The answer to " + operation + " (HTTP " + answer.status + ") holds a value this client cannot read: " +
255
+ (error instanceof Error ? error.message : String(error)),
256
+ answer.status,
257
+ );
258
+ }
259
+ }
260
+ `,
261
+ },
262
+ ];
263
+ }
@@ -1,7 +1,12 @@
1
1
  export declare const GENERATED_OWNERSHIP_LINE = "/* !!! Generated by @aventara/client. Do not edit. !!! */";
2
+ /** The command a generated file says to regenerate it with: a command in backticks, then any words about it. */
3
+ export type RegenerateCommand = `\`${string}\`${string}`;
4
+ /** `avclient generate`'s own: what its banner and errors say. */
5
+ export declare const AVCLIENT_REGENERATE: RegenerateCommand;
6
+ /** `avclient generate`'s banner. */
2
7
  export declare const GENERATED_BANNER: readonly string[];
3
8
  /** `source` with the banner before it and one blank line between. */
4
- export declare function withGeneratedBanner(source: string): string;
9
+ export declare function withGeneratedBanner(source: string, regenerate?: RegenerateCommand): string;
5
10
  /**
6
11
  * How far into a file the ownership line is looked for. The banner opens every
7
12
  * emitted file, so the line sits well inside this; a reader never has to read a
@@ -1,12 +1,16 @@
1
1
  export const GENERATED_OWNERSHIP_LINE = "/* !!! Generated by @aventara/client. Do not edit. !!! */";
2
- export const GENERATED_BANNER = [
3
- "// biome-ignore-all format: generated output",
4
- "// biome-ignore-all lint: generated output",
5
- GENERATED_OWNERSHIP_LINE,
6
- "/* Regenerate with `avclient generate`. */",
7
- ];
8
- export function withGeneratedBanner(source) {
9
- return `${GENERATED_BANNER.join("\n")}\n\n${source}`;
2
+ export const AVCLIENT_REGENERATE = "`avclient generate`";
3
+ function generatedBanner(regenerate = AVCLIENT_REGENERATE) {
4
+ return [
5
+ "// biome-ignore-all format: generated output",
6
+ "// biome-ignore-all lint: generated output",
7
+ GENERATED_OWNERSHIP_LINE,
8
+ `/* Regenerate with ${regenerate}. */`,
9
+ ];
10
+ }
11
+ export const GENERATED_BANNER = generatedBanner();
12
+ export function withGeneratedBanner(source, regenerate = AVCLIENT_REGENERATE) {
13
+ return `${generatedBanner(regenerate).join("\n")}\n\n${source}`;
10
14
  }
11
15
  export const GENERATED_OWNERSHIP_HEAD_BYTES = 1024;
12
16
  export function carriesGeneratedOwnership(head) {
@@ -1,5 +1,7 @@
1
+ import { ACCESS_VERB_NAMES } from "@aventara/core/internal";
2
+ import { accessVerbShape, accessVerbsOf, } from "./access.emitter.js";
1
3
  import { contractHasFieldChecks } from "./field-check.emitter.js";
2
- import { shapedContractType } from "./json-shape.emitter.js";
4
+ import { jsonSchemaType, shapedContractType } from "./json-shape.emitter.js";
3
5
  import { importSpecifier, } from "./module-style.interface.js";
4
6
  import { ownPropertyKey } from "./name.deriver.js";
5
7
  export function emitClientSurfaceModule(contract, names, style) {
@@ -18,6 +20,58 @@ export function emitClientSurfaceModule(contract, names, style) {
18
20
  const renamedEntries = renamed.length === 0
19
21
  ? ""
20
22
  : ` ${renamed.map((property) => `${ownPropertyKey(property.contractName)}: ${JSON.stringify(property.property)}`).join(", ")} `;
23
+ const access = contract.access;
24
+ const verbs = accessVerbsOf(contract);
25
+ const argsType = (name) => {
26
+ const schema = access?.verbs?.[name]?.args;
27
+ return schema === undefined
28
+ ? "undefined"
29
+ : jsonSchemaType(schema);
30
+ };
31
+ const argsMembers = ACCESS_VERB_NAMES.filter((name) => accessVerbShape(name).args)
32
+ .map((name) => `readonly ${name}: ${argsType(name)}`)
33
+ .join("; ");
34
+ const accessTypes = access === undefined
35
+ ? ""
36
+ : "\n" +
37
+ "/** One question `$access.can` takes: an action on a Resource, or on the one row its unique `where` names. */\n" +
38
+ "type AccessQuestion = {\n" +
39
+ "\treadonly [R in Resource]: {\n" +
40
+ "\t\treadonly resource: R;\n" +
41
+ '\t\treadonly action: "read" | "create" | "update" | "delete";\n' +
42
+ `\t\treadonly where?: AvUniqueWhereInput<${C}, R, ClientScalarForms>;\n` +
43
+ "\t};\n" +
44
+ "}[Resource];\n" +
45
+ "\n" +
46
+ "/** `avClient.$access`: `can`, and the Aventara Access verbs this deployment offers. */\n" +
47
+ "type AccessSurface = Pick<\n" +
48
+ "\tAvAccessVerbs<\n" +
49
+ "\t\tAccessQuestion,\n" +
50
+ `\t\t${verbs.includes("me") ? "AvAccessMe" : "never"},\n` +
51
+ `\t\t{ ${argsMembers} },\n` +
52
+ "\t\tAvCallOptions\n" +
53
+ "\t>,\n" +
54
+ `\t${["can", ...verbs].map((name) => JSON.stringify(name)).join(" | ")}\n` +
55
+ ">;\n";
56
+ const accessMember = (name) => {
57
+ const shape = accessVerbShape(name);
58
+ const quoted = JSON.stringify(name);
59
+ const parameters = shape.args
60
+ ? "args: unknown, callOptions: AvCallOptions = {}"
61
+ : "callOptions: AvCallOptions = {}";
62
+ const args = shape.args ? "args" : "undefined";
63
+ return shape.resolves === "nothing"
64
+ ? `\t\t\t\t${name}: async (${parameters}): Promise<void> => {\n\t\t\t\t\tawait callAccess(connect(), ${quoted}, ${args}, callOptions);\n\t\t\t\t},\n`
65
+ : `\t\t\t\t${name}: async (${parameters}) => callAccess(connect(), ${quoted}, ${args}, callOptions),\n`;
66
+ };
67
+ const accessRuntime = access === undefined
68
+ ? ""
69
+ : '\t\tObject.defineProperty(this, "$access", {\n' +
70
+ "\t\t\tvalue: Object.freeze({\n" +
71
+ "\t\t\t\tcan: async (questions: readonly unknown[], callOptions: AvCallOptions = {}) => askCan(connect(), questions, callOptions),\n" +
72
+ verbs.map(accessMember).join("") +
73
+ "\t\t\t}),\n" +
74
+ "\t\t});\n";
21
75
  const surface = (name, call) => `type ${name} = {\n` +
22
76
  `\treadonly [${resourceKey}]: {\n` +
23
77
  "\t\treadonly [F in keyof Grammar[R]]: {\n" +
@@ -32,14 +86,22 @@ export function emitClientSurfaceModule(contract, names, style) {
32
86
  ? ""
33
87
  : `import type { JsonFieldType } from ${from("./derivation/contracts/json-shape")};\n`) +
34
88
  `import type { AvApplicationScalarForms } from ${from("./derivation/contracts/scalar-value-type")};\n` +
35
- `import type { AvOperationArgumentsFor, AvResourceKey } from ${from("./derivation/operations/operation-arguments")};\n` +
89
+ (access === undefined
90
+ ? `import type { AvOperationArgumentsFor, AvResourceKey } from ${from("./derivation/operations/operation-arguments")};\n`
91
+ : `import type { AvOperationArgumentsFor, AvResourceKey, AvUniqueWhereInput } from ${from("./derivation/operations/operation-arguments")};\n`) +
36
92
  `import type { ${interactive ? "AvDeferredOperationCall, " : ""}AvOperationCall, AvOperationGrammar } from ${from("./derivation/operations/operation-call")};\n` +
37
93
  `import type { AvOperationFamily, AvOperationIdentity } from ${from("./derivation/operations/operation-identity")};\n` +
38
94
  `import type { AdmittedOperationResult } from ${from("./derivation/operations/operation-result")};\n` +
39
95
  (interactive
40
96
  ? `import type { AvOperation, AvTransactionResults } from ${from("./derivation/transactions/deferred-operation")};\n`
41
97
  : "") +
98
+ (access === undefined
99
+ ? ""
100
+ : `import type { AvAccessVerbs } from ${from("./derivation/policy/access-verbs")};\n`) +
42
101
  `import { DEFAULT_ENTRYPOINT } from ${from("./metadata")};\n` +
102
+ (verbs.length === 0
103
+ ? ""
104
+ : `import { callAccess } from ${from("./runtime/access")};\n`) +
43
105
  `import type { AvDecimal } from ${from("./runtime/decimal")};\n` +
44
106
  `import { ADVERTISED_OPERATIONS } from ${from("./runtime/descriptor")};\n` +
45
107
  (checked
@@ -48,7 +110,12 @@ export function emitClientSurfaceModule(contract, names, style) {
48
110
  (interactive
49
111
  ? `import { deferOperation, runTransaction } from ${from("./runtime/transaction")};\n`
50
112
  : "") +
51
- `import { type AvCallOptions, execute, type AvFetch, type TransportConnection } from ${from("./runtime/transport")};\n` +
113
+ (access === undefined
114
+ ? `import { type AvCallOptions, execute, type AvFetch, type TransportConnection } from ${from("./runtime/transport")};\n`
115
+ : `import { askCan, type AvCallOptions, execute, type AvFetch, type TransportConnection } from ${from("./runtime/transport")};\n`) +
116
+ (verbs.includes("me")
117
+ ? `import type { AvAccessMe } from ${from("./types")};\n`
118
+ : "") +
52
119
  "\n" +
53
120
  `export type { AvCallOptions } from ${from("./runtime/transport")};\n` +
54
121
  "\n" +
@@ -102,6 +169,7 @@ export function emitClientSurfaceModule(contract, names, style) {
102
169
  "\toptions?: AvCallOptions,\n" +
103
170
  ") => Promise<AvTransactionResults<Steps>>;\n"
104
171
  : "") +
172
+ accessTypes +
105
173
  "\n" +
106
174
  "/** The generated client: one property per Resource this deployment's ClientContract advertises. */\n" +
107
175
  "export interface AvClient extends AvClientSurface {}\n" +
@@ -120,6 +188,11 @@ export function emitClientSurfaceModule(contract, names, style) {
120
188
  " */\n"
121
189
  : "/** The generated client. */\n") +
122
190
  "export class AvClient {\n" +
191
+ (access === undefined
192
+ ? ""
193
+ : "\t/** The policy's answers for the UI (`can`), and the Aventara Access verbs this deployment offers. */\n" +
194
+ "\tdeclare readonly $access: AccessSurface;\n" +
195
+ (interactive ? "" : "\n")) +
123
196
  (interactive
124
197
  ? "\t/** Deferred operations, for `transaction`. */\n" +
125
198
  "\tdeclare readonly tx: AvTxSurface;\n" +
@@ -134,10 +207,8 @@ export function emitClientSurfaceModule(contract, names, style) {
134
207
  "\t\tfor (const [resource, families] of operationTree(\n" +
135
208
  "\t\t\t(resource, family, variant): OperationMethod =>\n" +
136
209
  (checked
137
- ? "\t\t\t\tasync (args = {}, callOptions = {}) => {\n" +
138
- "\t\t\t\t\tcheckOperation(resource, family, args);\n" +
139
- "\t\t\t\t\treturn execute(connect(), resource, family, variant, args, callOptions);\n" +
140
- "\t\t\t\t},\n"
210
+ ? "\t\t\t\tasync (args = {}, callOptions = {}) =>\n" +
211
+ "\t\t\t\t\texecute(connect(), resource, family, variant, args, callOptions, checkOperation),\n"
141
212
  : "\t\t\t\tasync (args = {}, callOptions = {}) =>\n" +
142
213
  "\t\t\t\t\texecute(connect(), resource, family, variant, args, callOptions),\n") +
143
214
  "\t\t)) {\n" +
@@ -160,6 +231,7 @@ export function emitClientSurfaceModule(contract, names, style) {
160
231
  : "\t\t\t\trunTransaction(connect(), steps, callOptions),\n") +
161
232
  "\t\t});\n"
162
233
  : "") +
234
+ accessRuntime +
163
235
  "\t}\n" +
164
236
  "}\n" +
165
237
  "\n" +
@@ -1,8 +1,15 @@
1
1
  import type { AvContractClient } from "@aventara/core";
2
- import type { ClientEntrypoint } from "../config/client-config.interface.js";
2
+ import type { EmittedEntrypoint } from "../config/client-config.interface.js";
3
+ import { type RegenerateCommand } from "./banner.emitter.js";
3
4
  import { type ClientEmission } from "./emitted-tree.interface.js";
4
5
  import { type ClientModuleStyle } from "./module-style.interface.js";
5
6
  /**
6
7
  * @throws AvGeneratedNameError when a name cannot be emitted, even renamed.
8
+ * @throws AvClientConfigError when the ClientContract's Access URL is on an
9
+ * origin other than the entrypoint's that `accessOrigins` does not list.
7
10
  */
8
- export declare function emitClientTree(contract: AvContractClient, entrypoint: ClientEntrypoint, style: ClientModuleStyle): ClientEmission;
11
+ export declare function emitClientTree(contract: AvContractClient, entrypoint: EmittedEntrypoint, style: ClientModuleStyle, regenerate?: RegenerateCommand, accessOrigins?: readonly string[]): ClientEmission;
12
+ /** How an entry re-exports `AvClient`: the class itself, or its type alone. */
13
+ export type ClientClassExport = "value" | "type";
14
+ /** The entry's re-exports, its default aside, as source: one statement per module. */
15
+ export declare function clientEntryReExports(style: ClientModuleStyle, clientClass?: ClientClassExport, accessVerbs?: boolean): string;