@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.
- package/CHANGELOG.md +194 -0
- package/README.md +37 -1
- package/dist/cli/command.parser.d.ts +4 -3
- package/dist/cli/command.parser.js +3 -3
- package/dist/cli/generation-failure.renderer.d.ts +2 -1
- package/dist/cli/generation-success.renderer.d.ts +2 -1
- package/dist/cli/step.reporter.d.ts +2 -1
- package/dist/cli/terminal.prompter.d.ts +2 -1
- package/dist/config/client-config.interface.d.ts +17 -0
- package/dist/config/config.loader.d.ts +8 -3
- package/dist/config/config.loader.js +20 -2
- package/dist/config/config.resolver.d.ts +10 -3
- package/dist/config/config.resolver.js +28 -0
- package/dist/config/env.cascade.d.ts +2 -6
- package/dist/config/env.cascade.js +3 -3
- package/dist/config/module-style.resolver.d.ts +3 -6
- package/dist/config/module-style.resolver.js +2 -2
- package/dist/config/tsconfig.locator.d.ts +2 -1
- package/dist/config/tsconfig.locator.js +14 -2
- package/dist/contract/contract.acceptance.d.ts +0 -1
- package/dist/contract/contract.acceptance.js +1 -1
- package/dist/contract/contract.fetcher.d.ts +2 -1
- package/dist/contract/contract.loader.d.ts +3 -2
- package/dist/emit/access.emitter.d.ts +12 -0
- package/dist/emit/access.emitter.js +263 -0
- package/dist/emit/banner.emitter.d.ts +6 -1
- package/dist/emit/banner.emitter.js +12 -8
- package/dist/emit/client-surface.emitter.js +79 -7
- package/dist/emit/client-tree.emitter.d.ts +9 -2
- package/dist/emit/client-tree.emitter.js +45 -14
- package/dist/emit/derivation.emitter.d.ts +3 -3
- package/dist/emit/derivation.emitter.js +6 -3
- package/dist/emit/emitted-tree.interface.d.ts +1 -1
- package/dist/emit/field-check.emitter.d.ts +2 -1
- package/dist/emit/field-check.emitter.js +2 -13
- package/dist/emit/module-specifier.scanner.d.ts +2 -1
- package/dist/emit/name.deriver.d.ts +2 -3
- package/dist/emit/name.deriver.js +4 -1
- package/dist/emit/named-type.emitter.js +12 -2
- package/dist/emit/runtime.emitter.d.ts +5 -4
- package/dist/emit/runtime.emitter.js +101 -7
- package/dist/emit/scalar.codec.js +223 -6
- package/dist/emit/transaction.emitter.js +7 -3
- package/dist/generate.d.ts +3 -2
- package/dist/generate.js +1 -1
- package/dist/hydration/hydration-tag.format.d.ts +11 -0
- package/dist/hydration/hydration-tag.format.js +6 -0
- package/dist/init/client-init.planner.d.ts +3 -2
- package/dist/init/client-init.questions.d.ts +1 -12
- package/dist/init/client-init.questions.js +2 -2
- package/dist/internal.d.ts +25 -0
- package/dist/internal.js +11 -0
- package/dist/output/output.validator.d.ts +3 -4
- package/dist/output/output.validator.js +2 -2
- package/dist/output/output.writer.d.ts +3 -2
- package/package.json +9 -5
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AvClientConfigError } from "../config/config.resolver.js";
|
|
2
|
+
import { accessVerbsOf, emitAccessModules } from "./access.emitter.js";
|
|
3
|
+
import { AVCLIENT_REGENERATE, withGeneratedBanner, } from "./banner.emitter.js";
|
|
2
4
|
import { emitClientSurfaceModule } from "./client-surface.emitter.js";
|
|
3
5
|
import { emitContractCarrierModule } from "./contract-carrier.emitter.js";
|
|
4
|
-
import { emitDerivationModules } from "./derivation.emitter.js";
|
|
6
|
+
import { ACCESS_DERIVATION_ROOTS, DERIVATION_ROOTS, emitDerivationModules, } from "./derivation.emitter.js";
|
|
5
7
|
import { emitDescriptorModule } from "./descriptor.emitter.js";
|
|
6
8
|
import { CLIENT_ENTRY_FILE, GENERATED_DIRECTORY, } from "./emitted-tree.interface.js";
|
|
7
9
|
import { emitEnumsModule } from "./enum.emitter.js";
|
|
@@ -12,9 +14,12 @@ import { emitNamedTypesModule } from "./named-type.emitter.js";
|
|
|
12
14
|
import { emitDecimalModule, emitErrorsModule, emitMetadataModule, emitTransportModule, errorsModuleExports, } from "./runtime.emitter.js";
|
|
13
15
|
import { emitScalarCodecModule } from "./scalar.codec.js";
|
|
14
16
|
import { emitTransactionModules } from "./transaction.emitter.js";
|
|
15
|
-
export function emitClientTree(contract, entrypoint, style) {
|
|
17
|
+
export function emitClientTree(contract, entrypoint, style, regenerate = AVCLIENT_REGENERATE, accessOrigins = []) {
|
|
18
|
+
refuseForeignAccessOrigin(contract, entrypoint, accessOrigins);
|
|
16
19
|
const names = deriveEmittedNames(contract);
|
|
17
|
-
const derivation = emitDerivationModules(
|
|
20
|
+
const derivation = emitDerivationModules(undefined, contract.access === undefined
|
|
21
|
+
? DERIVATION_ROOTS
|
|
22
|
+
: [...DERIVATION_ROOTS, ...ACCESS_DERIVATION_ROOTS]);
|
|
18
23
|
const generated = [
|
|
19
24
|
...derivation,
|
|
20
25
|
emitClientSurfaceModule(contract, names, style),
|
|
@@ -24,8 +29,9 @@ export function emitClientTree(contract, entrypoint, style) {
|
|
|
24
29
|
emitMetadataModule(contract.protocol, entrypoint),
|
|
25
30
|
emitScalarCodecModule(style),
|
|
26
31
|
emitDecimalModule(),
|
|
27
|
-
emitErrorsModule(),
|
|
28
|
-
emitTransportModule(style),
|
|
32
|
+
emitErrorsModule(regenerate),
|
|
33
|
+
emitTransportModule(style, contract.access !== undefined),
|
|
34
|
+
...emitAccessModules(contract, style),
|
|
29
35
|
emitNamedTypesModule(contract, names, style),
|
|
30
36
|
...(contract.transactions === "interactive"
|
|
31
37
|
? emitTransactionModules(style)
|
|
@@ -33,7 +39,10 @@ export function emitClientTree(contract, entrypoint, style) {
|
|
|
33
39
|
...emitFieldCheckModules(contract, style, new Set(derivation.map((module) => module.path))),
|
|
34
40
|
];
|
|
35
41
|
const files = [
|
|
36
|
-
{
|
|
42
|
+
{
|
|
43
|
+
path: CLIENT_ENTRY_FILE,
|
|
44
|
+
source: clientEntrySource(style, accessVerbsOf(contract).length > 0),
|
|
45
|
+
},
|
|
37
46
|
...generated.map((module) => ({
|
|
38
47
|
path: `${GENERATED_DIRECTORY}/${module.path}`,
|
|
39
48
|
source: module.source,
|
|
@@ -44,15 +53,22 @@ export function emitClientTree(contract, entrypoint, style) {
|
|
|
44
53
|
.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0)
|
|
45
54
|
.map((file) => ({
|
|
46
55
|
path: file.path,
|
|
47
|
-
bytes: encoder.encode(withGeneratedBanner(file.source)),
|
|
56
|
+
bytes: encoder.encode(withGeneratedBanner(file.source, regenerate)),
|
|
48
57
|
}));
|
|
49
58
|
return { tree, style, warnings: names.warnings };
|
|
50
59
|
}
|
|
51
|
-
function clientEntrySource(style) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
60
|
+
function clientEntrySource(style, accessVerbs) {
|
|
61
|
+
return (`export { default } from ${generatedModuleSpecifier(style, "client.ts")};\n` +
|
|
62
|
+
clientEntryReExports(style, "value", accessVerbs));
|
|
63
|
+
}
|
|
64
|
+
function generatedModuleSpecifier(style, module) {
|
|
65
|
+
return moduleSpecifierWriter(style)(`./${GENERATED_DIRECTORY}/${module.replace(/\.d\.ts$|\.ts$/, "")}`);
|
|
66
|
+
}
|
|
67
|
+
export function clientEntryReExports(style, clientClass = "value", accessVerbs = false) {
|
|
68
|
+
const from = (module) => generatedModuleSpecifier(style, module);
|
|
69
|
+
return ((clientClass === "value"
|
|
70
|
+
? `export {\n\tAvClient,\n\ttype AvClientOptions,\n\ttype AvCallOptions,\n} from ${from("client.ts")};\n`
|
|
71
|
+
: `export type { AvClient, AvClientOptions, AvCallOptions } from ${from("client.ts")};\n`) +
|
|
56
72
|
`export type { AvFetch } from ${from("runtime/transport.ts")};\n` +
|
|
57
73
|
`export type { AvOperation } from ${from("derivation/transactions/deferred-operation.d.ts")};\n` +
|
|
58
74
|
`export type * from ${from("types.d.ts")};\n` +
|
|
@@ -60,5 +76,20 @@ function clientEntrySource(style) {
|
|
|
60
76
|
`export { AvDecimal } from ${from("runtime/decimal.ts")};\n` +
|
|
61
77
|
`export {\n${errorsModuleExports()
|
|
62
78
|
.map((name) => `\t${name},\n`)
|
|
63
|
-
.join("")}} from ${from("runtime/errors.ts")};\n`
|
|
79
|
+
.join("")}} from ${from("runtime/errors.ts")};\n` +
|
|
80
|
+
(accessVerbs
|
|
81
|
+
? `export { AvAccessError } from ${from("runtime/access.ts")};\n`
|
|
82
|
+
: ""));
|
|
83
|
+
}
|
|
84
|
+
function refuseForeignAccessOrigin(contract, entrypoint, accessOrigins) {
|
|
85
|
+
const url = contract.access?.url;
|
|
86
|
+
if (url === undefined || url.startsWith("/")) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const origin = new URL(url).origin;
|
|
90
|
+
if (origin === entrypoint.deployment?.origin ||
|
|
91
|
+
accessOrigins.includes(origin)) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
throw new AvClientConfigError(`the ClientContract's Aventara Access URL is on ${origin}, not on the entrypoint's origin; $access sends sign-in credentials there, so it is emitted only when avclient's framework.client.ts lists that origin in accessOrigins (a same-origin client from aventara-build --client takes none: mount Access on the app's own origin).`);
|
|
64
95
|
}
|
|
@@ -5,8 +5,6 @@ export declare const DERIVATION_ROOTS: readonly string[];
|
|
|
5
5
|
* (POSIX-separated); `undefined` when there is none.
|
|
6
6
|
*/
|
|
7
7
|
export type DeclarationReader = (relative: string) => string | undefined;
|
|
8
|
-
/** The published declarations of the `@aventara/core` this package resolves. */
|
|
9
|
-
export declare function publishedCoreDeclarations(): DeclarationReader;
|
|
10
8
|
/**
|
|
11
9
|
* Core's declaration closure from {@link DERIVATION_ROOTS}, as modules under
|
|
12
10
|
* `derivation/`, in UTF-16 code-unit order of their path.
|
|
@@ -14,4 +12,6 @@ export declare function publishedCoreDeclarations(): DeclarationReader;
|
|
|
14
12
|
* @throws Error when the closure cannot be transported — a defect of the
|
|
15
13
|
* installed `@aventara/core`, never of the consumer's input.
|
|
16
14
|
*/
|
|
17
|
-
|
|
15
|
+
/** The roots a client with the `access` section copies besides {@link DERIVATION_ROOTS}. */
|
|
16
|
+
export declare const ACCESS_DERIVATION_ROOTS: readonly string[];
|
|
17
|
+
export declare function emitDerivationModules(read?: DeclarationReader, roots?: readonly string[]): readonly EmittedModule[];
|
|
@@ -13,7 +13,7 @@ export const DERIVATION_ROOTS = [
|
|
|
13
13
|
"transactions/deferred-operation",
|
|
14
14
|
"transactions/transaction-reference",
|
|
15
15
|
];
|
|
16
|
-
|
|
16
|
+
function publishedCoreDeclarations() {
|
|
17
17
|
const dist = path.dirname(fileURLToPath(import.meta.resolve("@aventara/core")));
|
|
18
18
|
return (relative) => {
|
|
19
19
|
try {
|
|
@@ -27,9 +27,12 @@ export function publishedCoreDeclarations() {
|
|
|
27
27
|
}
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
-
export
|
|
30
|
+
export const ACCESS_DERIVATION_ROOTS = [
|
|
31
|
+
"policy/access-verbs",
|
|
32
|
+
];
|
|
33
|
+
export function emitDerivationModules(read = publishedCoreDeclarations(), roots = DERIVATION_ROOTS) {
|
|
31
34
|
const copied = new Map();
|
|
32
|
-
const queue =
|
|
35
|
+
const queue = roots.map((root) => `${root}.d.ts`);
|
|
33
36
|
while (queue.length > 0) {
|
|
34
37
|
const file = queue.shift();
|
|
35
38
|
if (copied.has(file)) {
|
|
@@ -8,7 +8,7 @@ export declare const LEGACY_CLIENT_ENTRY_FILES: readonly string[];
|
|
|
8
8
|
/** The directory holding every other emitted module, under `generateAt`. */
|
|
9
9
|
export declare const GENERATED_DIRECTORY = "generated";
|
|
10
10
|
export type DerivationModulePath = `derivation/${string}.d.ts`;
|
|
11
|
-
export type GeneratedModulePath = DerivationModulePath | "client.ts" | "contract.ts" | "enums.ts" | "metadata.ts" | "runtime/codec.ts" | "runtime/decimal.ts" | "runtime/descriptor.ts" | "runtime/errors.ts" | "runtime/field-check.evaluator.ts" | "runtime/field-validators.ts" | "runtime/fingerprint.ts" | "runtime/transaction.ts" | "runtime/transport.ts" | "types.d.ts";
|
|
11
|
+
export type GeneratedModulePath = DerivationModulePath | "client.ts" | "contract.ts" | "enums.ts" | "metadata.ts" | "runtime/access.ts" | "runtime/codec.ts" | "runtime/decimal.ts" | "runtime/descriptor.ts" | "runtime/errors.ts" | "runtime/field-check.evaluator.ts" | "runtime/field-validators.ts" | "runtime/fingerprint.ts" | "runtime/transaction.ts" | "runtime/transport.ts" | "types.d.ts";
|
|
12
12
|
export declare const CONTRACT_CARRIER_MODULE = "contract.ts";
|
|
13
13
|
/** A path in the emitted tree: relative to `generateAt`, POSIX-separated. */
|
|
14
14
|
export type EmittedFilePath = typeof CLIENT_ENTRY_FILE | `${typeof GENERATED_DIRECTORY}/${GeneratedModulePath}`;
|
|
@@ -4,7 +4,7 @@ import { type ClientModuleStyle } from "./module-style.interface.js";
|
|
|
4
4
|
/** Where `@aventara/core` publishes the evaluator's source, relative to its package root. */
|
|
5
5
|
export declare const EVALUATOR_SOURCE = "src/runtime/field-check.evaluator.ts";
|
|
6
6
|
/** Reads the evaluator's published source; `undefined` when core publishes none. */
|
|
7
|
-
|
|
7
|
+
type EvaluatorSourceReader = () => string | undefined;
|
|
8
8
|
/** The evaluator's source as the `@aventara/core` this package resolves publishes it. */
|
|
9
9
|
export declare function publishedCoreEvaluator(): EvaluatorSourceReader;
|
|
10
10
|
/** Whether the ClientContract carries any check: a field's validators or a Json Shape node's. */
|
|
@@ -19,3 +19,4 @@ export declare function contractHasFieldChecks(contract: AvContractClient): bool
|
|
|
19
19
|
* `@aventara/core`, never of the consumer's input.
|
|
20
20
|
*/
|
|
21
21
|
export declare function emitFieldCheckModules(contract: AvContractClient, style: ClientModuleStyle, derivation: ReadonlySet<string>, read?: EvaluatorSourceReader): readonly EmittedModule[];
|
|
22
|
+
export {};
|
|
@@ -141,6 +141,7 @@ function rulesTableSource(contract) {
|
|
|
141
141
|
function fieldValidatorsSource(contract, from) {
|
|
142
142
|
return `import type { AvFieldCheck } from ${from("../derivation/contracts/field-check")};
|
|
143
143
|
import type { JsonSchema } from ${from("../derivation/contracts/json-shape")};
|
|
144
|
+
import { RECORD_SITES, writeKeysOf } from ${from("./codec")};
|
|
144
145
|
import { DECODE_TABLE } from ${from("./descriptor")};
|
|
145
146
|
import { type AvCause, type AvValidationIssue, frameworkErrorOf } from ${from("./errors")};
|
|
146
147
|
import {
|
|
@@ -456,21 +457,9 @@ function shapedValueIssues(schema: JsonSchema, list: boolean, value: unknown, pa
|
|
|
456
457
|
|
|
457
458
|
// ---- field validators, judged as the server judges them after its field pipes ----
|
|
458
459
|
|
|
459
|
-
/** Where each relation directive holds the records its target writes. */
|
|
460
|
-
const RECORD_SITES: { readonly [directive: string]: readonly (string | undefined)[] } = {
|
|
461
|
-
$create: [undefined],
|
|
462
|
-
$connect: [],
|
|
463
|
-
$connectOrCreate: ["create"],
|
|
464
|
-
$disconnect: [],
|
|
465
|
-
$set: [],
|
|
466
|
-
$update: ["data"],
|
|
467
|
-
$delete: [],
|
|
468
|
-
$upsert: ["create", "update"],
|
|
469
|
-
};
|
|
470
|
-
|
|
471
460
|
/** The top-level data records of a create, update or upsert. */
|
|
472
461
|
function writeRecordPaths(family: string, args: unknown): Path[] {
|
|
473
|
-
const keys = family
|
|
462
|
+
const keys = writeKeysOf(family);
|
|
474
463
|
const records: Path[] = [];
|
|
475
464
|
if (!isPlainRecord(args)) {
|
|
476
465
|
return records;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
interface ScannedModule {
|
|
2
2
|
/**
|
|
3
3
|
* Every module specifier: a `from` clause's, a side-effect `import`'s, and an
|
|
4
4
|
* `import()` type query's.
|
|
@@ -8,3 +8,4 @@ export interface ScannedModule {
|
|
|
8
8
|
readonly references: boolean;
|
|
9
9
|
}
|
|
10
10
|
export declare function scanModuleSpecifiers(text: string): ScannedModule;
|
|
11
|
+
export {};
|
|
@@ -3,7 +3,7 @@ import type { AvContractClient, AvOperationFamily } from "@aventara/core";
|
|
|
3
3
|
* Name derivation and the rename ladder for the emitted tree.
|
|
4
4
|
*/
|
|
5
5
|
export declare const RESERVED_EMITTED_NAMES: readonly string[];
|
|
6
|
-
|
|
6
|
+
interface ResourceNamedType {
|
|
7
7
|
readonly suffix: string;
|
|
8
8
|
readonly family: AvOperationFamily;
|
|
9
9
|
readonly variant: string;
|
|
@@ -37,8 +37,6 @@ export interface EmittedNames {
|
|
|
37
37
|
readonly properties: readonly EmittedProperty[];
|
|
38
38
|
readonly warnings: readonly string[];
|
|
39
39
|
}
|
|
40
|
-
/** Whether `text` is an IdentifierName — the rule above, for every emitter. */
|
|
41
|
-
export declare function isIdentifierName(text: string): boolean;
|
|
42
40
|
/**
|
|
43
41
|
* The emitted names of a Contract's enums and Resources, renamed where a name
|
|
44
42
|
* cannot be declared as-is.
|
|
@@ -52,3 +50,4 @@ export declare class AvGeneratedNameError extends Error {
|
|
|
52
50
|
constructor(problems: readonly string[]);
|
|
53
51
|
}
|
|
54
52
|
export declare function ownPropertyKey(value: string): string;
|
|
53
|
+
export {};
|
|
@@ -2,6 +2,8 @@ import { isOperationVariantAvailable } from "@aventara/core/internal";
|
|
|
2
2
|
import { FRAMEWORK_ERROR_CLASS_NAMES } from "./runtime.emitter.js";
|
|
3
3
|
export const RESERVED_EMITTED_NAMES = [
|
|
4
4
|
...new Set([
|
|
5
|
+
"AvAccessError",
|
|
6
|
+
"AvAccessMe",
|
|
5
7
|
"AvClient",
|
|
6
8
|
"AvClientOptions",
|
|
7
9
|
"AvCallOptions",
|
|
@@ -46,6 +48,7 @@ export function namedTypesOf(operations) {
|
|
|
46
48
|
return RESOURCE_NAMED_TYPES.filter((named) => isOperationVariantAvailable(families[named.family]?.[named.variant]));
|
|
47
49
|
}
|
|
48
50
|
export const CLIENT_MEMBER_NAMES = [
|
|
51
|
+
"$access",
|
|
49
52
|
"__defineGetter__",
|
|
50
53
|
"__defineSetter__",
|
|
51
54
|
"__lookupGetter__",
|
|
@@ -63,7 +66,7 @@ export const CLIENT_MEMBER_NAMES = [
|
|
|
63
66
|
"valueOf",
|
|
64
67
|
];
|
|
65
68
|
const IDENTIFIER = /^[\p{ID_Start}$_][\p{ID_Continue}$\u200C\u200D]*$/u;
|
|
66
|
-
|
|
69
|
+
function isIdentifierName(text) {
|
|
67
70
|
return IDENTIFIER.test(text);
|
|
68
71
|
}
|
|
69
72
|
const RESERVED_WORDS = new Set([
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { jsonSchemaType } from "./json-shape.emitter.js";
|
|
1
2
|
import { importSpecifier, } from "./module-style.interface.js";
|
|
2
3
|
import { namedTypesOf } from "./name.deriver.js";
|
|
3
4
|
export function emitNamedTypesModule(contract, names, style) {
|
|
@@ -12,12 +13,21 @@ export function emitNamedTypesModule(contract, names, style) {
|
|
|
12
13
|
`export type ${identifier}${named.suffix} = ResourceArgument<${resource}, ${JSON.stringify(named.family)}, ${JSON.stringify(named.variant)}, ${JSON.stringify(named.argument)}>;\n`;
|
|
13
14
|
});
|
|
14
15
|
});
|
|
16
|
+
const me = contract.access?.verbs?.me?.result;
|
|
17
|
+
const identity = me === undefined
|
|
18
|
+
? []
|
|
19
|
+
: [
|
|
20
|
+
"/** What `avClient.$access.me()` resolves when someone is signed in: the token identity. */\n" +
|
|
21
|
+
`export type AvAccessMe = ${jsonSchemaType(me)};\n`,
|
|
22
|
+
];
|
|
15
23
|
const uses = (helper) => declarations.some((declaration) => declaration.includes(` = ${helper}<`));
|
|
16
24
|
const imported = ["ResourceArgument", "ResourceRecord"].filter(uses);
|
|
17
25
|
return {
|
|
18
26
|
path: "types.d.ts",
|
|
19
27
|
source: declarations.length === 0
|
|
20
|
-
?
|
|
21
|
-
|
|
28
|
+
? identity.length === 0
|
|
29
|
+
? "export {};\n"
|
|
30
|
+
: identity.join("")
|
|
31
|
+
: `import type { ${imported.join(", ")} } from ${JSON.stringify(importSpecifier("./client", style))};\n\n${declarations.join("")}${identity.join("")}`,
|
|
22
32
|
};
|
|
23
33
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { AvContractClient } from "@aventara/core";
|
|
2
|
-
import type {
|
|
2
|
+
import type { EmittedEntrypoint } from "../config/client-config.interface.js";
|
|
3
|
+
import { type RegenerateCommand } from "./banner.emitter.js";
|
|
3
4
|
import type { EmittedModule } from "./emitted-tree.interface.js";
|
|
4
5
|
import { type ClientModuleStyle } from "./module-style.interface.js";
|
|
5
6
|
export declare function emitDecimalModule(): EmittedModule;
|
|
6
|
-
export declare function emitMetadataModule(protocol: AvContractClient["protocol"], entrypoint:
|
|
7
|
+
export declare function emitMetadataModule(protocol: AvContractClient["protocol"], entrypoint: EmittedEntrypoint): EmittedModule;
|
|
7
8
|
export declare const FRAMEWORK_ERROR_CLASS_NAMES: readonly string[];
|
|
8
9
|
export declare function errorsModuleExports(): readonly string[];
|
|
9
|
-
export declare function emitErrorsModule(): EmittedModule;
|
|
10
|
-
export declare function emitTransportModule(style: ClientModuleStyle): EmittedModule;
|
|
10
|
+
export declare function emitErrorsModule(regenerate?: RegenerateCommand): EmittedModule;
|
|
11
|
+
export declare function emitTransportModule(style: ClientModuleStyle, access?: boolean): EmittedModule;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FRAMEWORK_ERROR_CLASS_OF, OPERATION_CODES, VALIDATION_CODES, } from "@aventara/core/internal";
|
|
2
2
|
import { AvProtocol } from "@aventara/core/protocol";
|
|
3
3
|
import { entrypointHref } from "../config/config.resolver.js";
|
|
4
|
+
import { AVCLIENT_REGENERATE, } from "./banner.emitter.js";
|
|
4
5
|
import { moduleSpecifierWriter, } from "./module-style.interface.js";
|
|
5
6
|
import { wireGrammarLiteral } from "./scalar.codec.js";
|
|
6
7
|
export function emitDecimalModule() {
|
|
@@ -51,10 +52,13 @@ export function emitMetadataModule(protocol, entrypoint) {
|
|
|
51
52
|
`export const DEFAULT_ENTRYPOINT = ${JSON.stringify(entrypointHref(entrypoint))};\n`,
|
|
52
53
|
};
|
|
53
54
|
}
|
|
55
|
+
function contractMismatchSummary(regenerate) {
|
|
56
|
+
return `This client was generated against a ClientContract other than the one the server serves. Regenerate it with ${regenerate}.`;
|
|
57
|
+
}
|
|
54
58
|
const FRAMEWORK_ERROR_CLASSES = {
|
|
55
59
|
AvAuthError: "The caller is not authenticated, or is not permitted to run the operation.",
|
|
56
60
|
AvConflictError: "The operation conflicted with stored state, and nothing was written. A2008 fails the same way if sent again; A2013 and A2014 may succeed if sent again, and no retry is automatic.",
|
|
57
|
-
AvContractMismatchError:
|
|
61
|
+
AvContractMismatchError: contractMismatchSummary(AVCLIENT_REGENERATE),
|
|
58
62
|
AvInternalError: "The server failed. Nothing in its cause is actionable; the diagnostics are in the server's logs.",
|
|
59
63
|
AvNotFoundError: "A strict unique target does not exist.",
|
|
60
64
|
AvProtocolError: "The request was not one the server could route or interpret: an unknown Resource or operation, a protocol version it does not speak, or a body, media type, size or method it refuses.",
|
|
@@ -152,8 +156,12 @@ export class AvTransportError extends Error {
|
|
|
152
156
|
}
|
|
153
157
|
}
|
|
154
158
|
`;
|
|
155
|
-
export function emitErrorsModule() {
|
|
156
|
-
const
|
|
159
|
+
export function emitErrorsModule(regenerate = AVCLIENT_REGENERATE) {
|
|
160
|
+
const summaries = {
|
|
161
|
+
...FRAMEWORK_ERROR_CLASSES,
|
|
162
|
+
AvContractMismatchError: contractMismatchSummary(regenerate),
|
|
163
|
+
};
|
|
164
|
+
const classes = Object.entries(summaries).map(([name, summary]) => {
|
|
157
165
|
const codes = ERROR_CODES.filter((code) => FRAMEWORK_ERROR_CLASS_OF[code] === name);
|
|
158
166
|
return (`\n/** ${summary} Thrown for ${codes.join(", ")}. */\n` +
|
|
159
167
|
`export class ${name} extends AvError<${codes.map((code) => JSON.stringify(code)).join(" | ")}> {\n` +
|
|
@@ -186,12 +194,49 @@ export function emitErrorsModule() {
|
|
|
186
194
|
"}\n",
|
|
187
195
|
};
|
|
188
196
|
}
|
|
189
|
-
export function emitTransportModule(style) {
|
|
197
|
+
export function emitTransportModule(style, access = false) {
|
|
190
198
|
return {
|
|
191
199
|
path: "runtime/transport.ts",
|
|
192
|
-
source: transportModuleSource(moduleSpecifierWriter(style))
|
|
200
|
+
source: transportModuleSource(moduleSpecifierWriter(style)) +
|
|
201
|
+
(access ? ASK_CAN_SOURCE : ""),
|
|
193
202
|
};
|
|
194
203
|
}
|
|
204
|
+
const ASK_CAN_SOURCE = `
|
|
205
|
+
/**
|
|
206
|
+
* Asks the policy every question in one request (\`POST /_access/can\`), each
|
|
207
|
+
* question's \`where\` in its wire forms, and resolves one boolean each, in order.
|
|
208
|
+
*
|
|
209
|
+
* @throws as {@link execute} does: the AvError subclass of a refusal (A2001 for a
|
|
210
|
+
* Resource this client cannot reach, A2004 for a malformed question, A2009 past
|
|
211
|
+
* the batch limit), AvTransportError when no framework envelope arrived.
|
|
212
|
+
*/
|
|
213
|
+
export async function askCan(
|
|
214
|
+
connection: TransportConnection,
|
|
215
|
+
questions: readonly unknown[],
|
|
216
|
+
options: AvCallOptions = {},
|
|
217
|
+
): Promise<boolean[]> {
|
|
218
|
+
const operation = "$access.can";
|
|
219
|
+
if (!Array.isArray(questions)) {
|
|
220
|
+
throw new TypeError("$access.can takes a list of questions.");
|
|
221
|
+
}
|
|
222
|
+
const headers = requestHeaders(options);
|
|
223
|
+
const body = serializeWireBody({
|
|
224
|
+
questions: questions.map((question: unknown) =>
|
|
225
|
+
isRecord(question) && Object.hasOwn(question, "where")
|
|
226
|
+
? { ...question, where: encodeArguments(question["where"]) }
|
|
227
|
+
: question,
|
|
228
|
+
),
|
|
229
|
+
});
|
|
230
|
+
const response = await post(connection, "/_access/can", operation, headers, body, options);
|
|
231
|
+
return decoded(operation, response.status, () => {
|
|
232
|
+
const data: unknown = response.data;
|
|
233
|
+
if (!Array.isArray(data) || data.length !== questions.length || !data.every((answer) => typeof answer === "boolean")) {
|
|
234
|
+
throw new TypeError("The answer must hold one boolean per question.");
|
|
235
|
+
}
|
|
236
|
+
return data as boolean[];
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
`;
|
|
195
240
|
const IDENTITY_HEADERS = (() => {
|
|
196
241
|
const { protocolVersion, contractHash, requestId } = AvProtocol.headers;
|
|
197
242
|
const prefix = protocolVersion.slice(0, protocolVersion.indexOf("-"));
|
|
@@ -209,7 +254,7 @@ const IDENTITY_HEADERS = (() => {
|
|
|
209
254
|
})();
|
|
210
255
|
function transportModuleSource(from) {
|
|
211
256
|
return `import { CLIENT_CONTRACT_HASH, PROTOCOL_VERSION } from ${from("../metadata")};
|
|
212
|
-
import { decodeResult, encodeArguments, readArgument, serializeWireBody } from ${from("./codec")};
|
|
257
|
+
import { decodeResult, encodeArguments, readArgument, serializeWireBody, unsentValues } from ${from("./codec")};
|
|
213
258
|
import { DECODE_TABLE } from ${from("./descriptor")};
|
|
214
259
|
import {
|
|
215
260
|
type AvCause,
|
|
@@ -319,12 +364,58 @@ type Envelope =
|
|
|
319
364
|
| { readonly outcome: "data"; readonly code: SuccessCode; readonly data: unknown }
|
|
320
365
|
| { readonly outcome: "failure"; readonly code: AvOperationErrorCode; readonly cause: AvCause };
|
|
321
366
|
|
|
367
|
+
/** What a refusal of an operation's arguments says. */
|
|
368
|
+
const REFUSAL_MESSAGE = "Operation arguments failed framework validation.";
|
|
369
|
+
|
|
370
|
+
/** What an undefined value the request would leave out is told. */
|
|
371
|
+
const UNSENT_MESSAGE =
|
|
372
|
+
"Value must not be undefined: sent as JSON it would be left out, and the request would reach more rows.";
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Refuses arguments that hold an undefined value whose leaving out would reach more
|
|
376
|
+
* rows than the caller wrote (in a filter, a unique selector or a cursor, or a where,
|
|
377
|
+
* cursor, limit or offset left undefined), before anything is sent: JSON would drop
|
|
378
|
+
* it. Throws the AvValidationError (A2004) the server answers when it is handed such
|
|
379
|
+
* a value: one issue at each, with the server's code, its path under \`at\`
|
|
380
|
+
* (["arguments"] for a call, ["operations", i, "args"] for a plan step, whose index
|
|
381
|
+
* the cause names).
|
|
382
|
+
*/
|
|
383
|
+
export function refuseUnsentValues(
|
|
384
|
+
resource: string,
|
|
385
|
+
family: string,
|
|
386
|
+
variant: string,
|
|
387
|
+
args: unknown,
|
|
388
|
+
at: readonly (string | number)[],
|
|
389
|
+
operation?: number,
|
|
390
|
+
): void {
|
|
391
|
+
const unsent = unsentValues(DECODE_TABLE, resource, family, variant, args);
|
|
392
|
+
if (unsent.length === 0) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const issues: readonly AvValidationIssue[] = Object.freeze(
|
|
396
|
+
unsent.map(({ path, code }) =>
|
|
397
|
+
Object.freeze({ path: Object.freeze([...at, ...path]), code, message: UNSENT_MESSAGE }),
|
|
398
|
+
),
|
|
399
|
+
);
|
|
400
|
+
throw frameworkErrorOf(
|
|
401
|
+
"A2004",
|
|
402
|
+
Object.freeze(
|
|
403
|
+
operation === undefined ? { message: REFUSAL_MESSAGE, issues } : { message: REFUSAL_MESSAGE, issues, operation },
|
|
404
|
+
),
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
322
408
|
/**
|
|
323
409
|
* Runs one operation: POSTs its arguments and settles its envelope.
|
|
324
410
|
*
|
|
411
|
+
* @param check the client's own checks of the arguments, run before anything is sent
|
|
412
|
+
* (the ClientContract's field validators, when it has any).
|
|
325
413
|
* @returns the envelope's data when the code is A1xxx; null for A1001.
|
|
326
414
|
* @throws the AvError subclass of any other code; AvTransportError when no
|
|
327
|
-
* framework envelope arrived;
|
|
415
|
+
* framework envelope arrived; AvValidationError (A2004), before anything is sent,
|
|
416
|
+
* when the arguments hold an undefined value JSON would leave out to widen the
|
|
417
|
+
* request (see refuseUnsentValues), or when
|
|
418
|
+
* \`check\` refuses the arguments; TypeError, before anything is sent, when the
|
|
328
419
|
* arguments are not a JSON-safe object or a request header is the framework's.
|
|
329
420
|
* A caller's abort rejects with what fetch rejected with.
|
|
330
421
|
*/
|
|
@@ -335,7 +426,10 @@ export async function execute(
|
|
|
335
426
|
variant: string,
|
|
336
427
|
args: Readonly<Record<string, unknown>>,
|
|
337
428
|
options: AvCallOptions = {},
|
|
429
|
+
check?: (resource: string, family: string, args: unknown) => void,
|
|
338
430
|
): Promise<unknown> {
|
|
431
|
+
refuseUnsentValues(resource, family, variant, args, ["arguments"]);
|
|
432
|
+
check?.(resource, family, args);
|
|
339
433
|
const operation = JSON.stringify(resource) + " " + family + "." + variant;
|
|
340
434
|
const headers = requestHeaders(options);
|
|
341
435
|
const body = serializeWireBody(encodeArguments(argumentObject(args)));
|