bazis 0.101.0 → 0.103.0
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/core/app/messages.ts +1 -1
- package/core/http/Binding/ParameterBinder.ts +113 -73
- package/core/http/Binding/convert.ts +0 -10
- package/core/http/Errors/messages.ts +0 -6
- package/core/http/README.md +6 -0
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/infra/connectors/codex.ts +8 -2
- package/core/infra/index.ts +1 -1
- package/package.json +1 -1
package/core/app/messages.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type AppLanguage = "en" | "ru";
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Own texts for framework messages, by key: HTTP texts (`notFound`,
|
|
10
|
-
* `
|
|
10
|
+
* `malformedJson`, …), validation codes (`required`, `minLength`, …),
|
|
11
11
|
* `validationFailed`, `unknownField` and type names (`typeName.number`).
|
|
12
12
|
* Placeholders: `{property}`, `{min}`, `{name}`, … as in the built-in texts.
|
|
13
13
|
*/
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { ListQueryError, parseListQuery } from "../../../library/jsonapi";
|
|
2
2
|
import { bindSourceModel } from "./sourceModelBinder";
|
|
3
|
-
import { UnsupportedMediaTypeError, frameworkBadRequest } from "../Errors/HttpError";
|
|
3
|
+
import { ModelValidationError, UnsupportedMediaTypeError, frameworkBadRequest } from "../Errors/HttpError";
|
|
4
4
|
import type { HttpContext } from "../HttpContext/HttpContext";
|
|
5
5
|
import type { ParameterBinding } from "./bindings";
|
|
6
|
-
import {
|
|
6
|
+
import { INVALID, tryConvert, type ValueType } from "./convert";
|
|
7
|
+
import type { ModelValidationIssue } from "./modelValidator";
|
|
7
8
|
import { bindModelAsync } from "./modelBinder";
|
|
8
9
|
|
|
9
10
|
/** Per-action constraints affecting binding (from decorators). */
|
|
@@ -26,7 +27,40 @@ export function assertConsumes(ctx: HttpContext, consumes: string | undefined):
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
/**
|
|
31
|
+
* One parameter that cannot be bound: an item of the single 400 that lists
|
|
32
|
+
* every binding problem of the request, like model validation errors.
|
|
33
|
+
*/
|
|
34
|
+
class ParameterIssue extends Error {
|
|
35
|
+
constructor(readonly issue: ModelValidationIssue) {
|
|
36
|
+
super(issue.message);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Type names in the `type` message: `int` stays, `bool` reads as `boolean`. */
|
|
41
|
+
const EXPECTED: Readonly<Record<ValueType, string>> = { string: "string", int: "int", number: "number", bool: "boolean" };
|
|
42
|
+
|
|
43
|
+
function issueOf(ctx: HttpContext, code: "required" | "type", params: Readonly<Record<string, unknown>>, english: string): ParameterIssue {
|
|
44
|
+
const message = ctx.modelValidator?.message?.(code, params) ?? english;
|
|
45
|
+
return new ParameterIssue({ property: params.property as string, message, code, params });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function missing(ctx: HttpContext, name: string): ParameterIssue {
|
|
49
|
+
return issueOf(ctx, "required", { property: name }, `Field "${name}" is required`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Converts a raw value or reports it as a `type` issue under the parameter name. */
|
|
53
|
+
function converted(ctx: HttpContext, raw: string, type: ValueType, name: string): string | number | boolean {
|
|
54
|
+
const value = tryConvert(raw, type);
|
|
55
|
+
if (value === INVALID) {
|
|
56
|
+
const expected = EXPECTED[type];
|
|
57
|
+
throw issueOf(ctx, "type", { property: name, expected, actual: "string" }, `Field "${name}" must be of type ${expected}, got: string`);
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function bindValue(binding: ParameterBinding, raw: string | undefined, ctx: HttpContext): unknown {
|
|
63
|
+
const name = binding.name ?? "?";
|
|
30
64
|
if (raw === undefined) {
|
|
31
65
|
if (binding.defaultValue !== undefined) {
|
|
32
66
|
return binding.defaultValue;
|
|
@@ -34,9 +68,9 @@ function bindValue(binding: ParameterBinding, raw: string | undefined, kind: str
|
|
|
34
68
|
if (binding.optional) {
|
|
35
69
|
return undefined;
|
|
36
70
|
}
|
|
37
|
-
throw
|
|
71
|
+
throw missing(ctx, name);
|
|
38
72
|
}
|
|
39
|
-
return binding.type && binding.type !== "string" ?
|
|
73
|
+
return binding.type && binding.type !== "string" ? converted(ctx, raw, binding.type, name) : raw;
|
|
40
74
|
}
|
|
41
75
|
|
|
42
76
|
/**
|
|
@@ -50,14 +84,17 @@ function bindArray(binding: ParameterBinding, ctx: HttpContext): unknown {
|
|
|
50
84
|
return binding.optional ? undefined : [];
|
|
51
85
|
}
|
|
52
86
|
return binding.type && binding.type !== "string"
|
|
53
|
-
? raw.map((value) =>
|
|
87
|
+
? raw.map((value) => converted(ctx, value, binding.type!, binding.name ?? "?"))
|
|
54
88
|
: raw;
|
|
55
89
|
}
|
|
56
90
|
|
|
57
91
|
/**
|
|
58
92
|
* Resolves generated binding descriptors into the action's argument list.
|
|
59
|
-
*
|
|
60
|
-
*
|
|
93
|
+
* Every argument is bound even when an earlier one fails: missing or invalid
|
|
94
|
+
* route, query and header values and the validation errors of body, query and
|
|
95
|
+
* header models come together in one 400 (`Validation failed` with
|
|
96
|
+
* `details`), like ASP.NET model state. Errors of the request itself
|
|
97
|
+
* (malformed JSON, wrong media type, too large) stop at once.
|
|
61
98
|
*/
|
|
62
99
|
export async function bindArguments(
|
|
63
100
|
bindings: readonly ParameterBinding[],
|
|
@@ -65,79 +102,82 @@ export async function bindArguments(
|
|
|
65
102
|
defaults: BindingDefaults,
|
|
66
103
|
): Promise<unknown[]> {
|
|
67
104
|
const args: unknown[] = new Array(bindings.length);
|
|
105
|
+
const issues: ModelValidationIssue[] = [];
|
|
106
|
+
let title: string | undefined;
|
|
68
107
|
for (let index = 0; index < bindings.length; index += 1) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
} else {
|
|
80
|
-
args[index] = value;
|
|
81
|
-
}
|
|
82
|
-
break;
|
|
108
|
+
try {
|
|
109
|
+
args[index] = await bindOne(bindings[index]!, ctx, defaults);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error instanceof ParameterIssue) {
|
|
112
|
+
issues.push(error.issue);
|
|
113
|
+
} else if (error instanceof ModelValidationError) {
|
|
114
|
+
issues.push(...error.errors);
|
|
115
|
+
title ??= error.message;
|
|
116
|
+
} else {
|
|
117
|
+
throw error;
|
|
83
118
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (issues.length > 0) {
|
|
122
|
+
throw new ModelValidationError(issues, title ?? ctx.modelValidator?.message?.("validationFailed", {}) ?? "Validation failed");
|
|
123
|
+
}
|
|
124
|
+
return args;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** One argument; a parameter problem is thrown as ParameterIssue or ModelValidationError. */
|
|
128
|
+
async function bindOne(binding: ParameterBinding, ctx: HttpContext, defaults: BindingDefaults): Promise<unknown> {
|
|
129
|
+
switch (binding.source) {
|
|
130
|
+
case "route": {
|
|
131
|
+
const value = binding.name !== undefined ? ctx.params[binding.name] : undefined;
|
|
132
|
+
// Route params are already constraint-converted; re-convert only when
|
|
133
|
+
// the binding asks for a different primitive type.
|
|
134
|
+
if (value === undefined) return bindValue(binding, undefined, ctx);
|
|
135
|
+
if (binding.type && binding.type !== "string" && typeof value === "string") return converted(ctx, value, binding.type, binding.name ?? "?");
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
case "query":
|
|
139
|
+
return binding.array ? bindArray(binding, ctx) : bindValue(binding, ctx.query(binding.name ?? ""), ctx);
|
|
140
|
+
case "header":
|
|
141
|
+
return bindValue(binding, ctx.header(binding.name ?? ""), ctx);
|
|
142
|
+
case "queryModel":
|
|
143
|
+
case "headerModel":
|
|
144
|
+
return bindSourceModel(binding, ctx);
|
|
145
|
+
case "body": {
|
|
146
|
+
const contentType = ctx.header("content-type") ?? "";
|
|
147
|
+
if (defaults.consumes) {
|
|
148
|
+
if (!contentType.toLowerCase().startsWith(defaults.consumes.toLowerCase())) {
|
|
149
|
+
throw new UnsupportedMediaTypeError(defaults.consumes);
|
|
105
150
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
151
|
+
} else if (!isJsonMediaType(contentType)) {
|
|
152
|
+
// Convention-based body-model binding is a JSON boundary. Requiring
|
|
153
|
+
// a non-simple media type also prevents cross-origin text/plain POSTs
|
|
154
|
+
// from bypassing the browser's CORS preflight.
|
|
155
|
+
throw new UnsupportedMediaTypeError("application/json");
|
|
109
156
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
throw error;
|
|
128
|
-
}
|
|
129
|
-
if (binding.model) {
|
|
130
|
-
// Class-based list request (extends ListRequest): instantiate and
|
|
131
|
-
// populate the parsed sort/filters/page so the action gets a typed object.
|
|
132
|
-
args[index] = Object.assign(new (binding.model as new () => object)(), parsed);
|
|
133
|
-
} else {
|
|
134
|
-
args[index] = parsed;
|
|
157
|
+
const data = await ctx.json();
|
|
158
|
+
return binding.model ? bindModelAsync(binding.model, data, ctx.modelValidator, { primitiveTypes: true }) : data;
|
|
159
|
+
}
|
|
160
|
+
case "request":
|
|
161
|
+
return ctx.request;
|
|
162
|
+
case "response":
|
|
163
|
+
return ctx.res;
|
|
164
|
+
case "context":
|
|
165
|
+
return ctx;
|
|
166
|
+
case "list": {
|
|
167
|
+
let parsed;
|
|
168
|
+
try {
|
|
169
|
+
parsed = parseListQuery(ctx.url.searchParams, binding.listOptions);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (error instanceof ListQueryError) {
|
|
172
|
+
throw frameworkBadRequest("invalidListQuery", undefined, error.problems);
|
|
135
173
|
}
|
|
136
|
-
|
|
174
|
+
throw error;
|
|
137
175
|
}
|
|
176
|
+
// Class-based list request (extends ListRequest): instantiate and
|
|
177
|
+
// populate the parsed sort/filters/page so the action gets a typed object.
|
|
178
|
+
return binding.model ? Object.assign(new (binding.model as new () => object)(), parsed) : parsed;
|
|
138
179
|
}
|
|
139
180
|
}
|
|
140
|
-
return args;
|
|
141
181
|
}
|
|
142
182
|
|
|
143
183
|
function isJsonMediaType(contentType: string): boolean {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { frameworkBadRequest } from "../Errors/HttpError";
|
|
2
1
|
|
|
3
2
|
/** Primitive conversion targets for query/header/route values. */
|
|
4
3
|
export type ValueType = "string" | "int" | "number" | "bool";
|
|
@@ -37,12 +36,3 @@ export function tryConvert(raw: string, type: ValueType): string | number | bool
|
|
|
37
36
|
return INVALID;
|
|
38
37
|
}
|
|
39
38
|
}
|
|
40
|
-
|
|
41
|
-
/** Converts or throws 400 with the parameter name in the message. */
|
|
42
|
-
export function convertOr400(raw: string, type: ValueType, parameterName: string): string | number | boolean {
|
|
43
|
-
const value = tryConvert(raw, type);
|
|
44
|
-
if (value === INVALID) {
|
|
45
|
-
throw frameworkBadRequest("invalidParameter", { name: parameterName, type, value: raw });
|
|
46
|
-
}
|
|
47
|
-
return value;
|
|
48
|
-
}
|
|
@@ -43,8 +43,6 @@ export type HttpMessageKey =
|
|
|
43
43
|
| "bodyTooDeep"
|
|
44
44
|
| "bodyTooComplex"
|
|
45
45
|
| "bodyCyclic"
|
|
46
|
-
| "missingParameter"
|
|
47
|
-
| "invalidParameter"
|
|
48
46
|
| "invalidListQuery";
|
|
49
47
|
|
|
50
48
|
export const EN_HTTP_MESSAGES: Readonly<Record<HttpMessageKey, string>> = Object.freeze({
|
|
@@ -75,8 +73,6 @@ export const EN_HTTP_MESSAGES: Readonly<Record<HttpMessageKey, string>> = Object
|
|
|
75
73
|
bodyTooDeep: "Request body is too deeply nested",
|
|
76
74
|
bodyTooComplex: "Request body is too complex",
|
|
77
75
|
bodyCyclic: "Request body must be acyclic JSON",
|
|
78
|
-
missingParameter: 'Missing required {kind} parameter "{name}"',
|
|
79
|
-
invalidParameter: 'Parameter "{name}" must be of type {type}, got: "{value}"',
|
|
80
76
|
invalidListQuery: "Invalid list query",
|
|
81
77
|
});
|
|
82
78
|
|
|
@@ -108,8 +104,6 @@ export const RU_HTTP_MESSAGES: Readonly<Record<HttpMessageKey, string>> = Object
|
|
|
108
104
|
bodyTooDeep: "Слишком глубокая вложенность в теле запроса",
|
|
109
105
|
bodyTooComplex: "Тело запроса слишком сложное",
|
|
110
106
|
bodyCyclic: "Тело запроса не должно содержать циклов",
|
|
111
|
-
missingParameter: 'Не передан обязательный параметр "{name}"',
|
|
112
|
-
invalidParameter: 'Параметр "{name}": ожидается {type}, получено: "{value}"',
|
|
113
107
|
invalidListQuery: "Некорректные параметры списка",
|
|
114
108
|
});
|
|
115
109
|
|
package/core/http/README.md
CHANGED
|
@@ -99,6 +99,12 @@ export class TenantHeaders {
|
|
|
99
99
|
- Misuse (`FromHeader<string[]>`, a name on a model, `FromRoute` outside the
|
|
100
100
|
template, `FromBody<string>`) is `BAZIS_HTTP_BINDING_UNRESOLVED`.
|
|
101
101
|
|
|
102
|
+
Every argument is bound even when another one fails: missing or invalid
|
|
103
|
+
route, query and header values and the validation errors of all models come
|
|
104
|
+
in one `400 Validation failed` with `details` (`code` `required`, `type`,
|
|
105
|
+
`duplicate`, rule codes), like ASP.NET model state. Errors of the request
|
|
106
|
+
itself (malformed JSON, media type, size) stop at once with their own text.
|
|
107
|
+
|
|
102
108
|
Checking allowed values belongs to the Validator or the service.
|
|
103
109
|
|
|
104
110
|
A `ctx: HttpContext` parameter stays a regular supported input. For older manual
|
|
@@ -232,7 +232,7 @@ export class CodexAppServerClient implements CodexClient {
|
|
|
232
232
|
if (method === "account/updated") this.modelCache = undefined;
|
|
233
233
|
}, () => { this.clearLogin(); this.modelCache = undefined; });
|
|
234
234
|
const initialized = await client.request("initialize", {
|
|
235
|
-
clientInfo: { name: "bazis", title: "Bazis", version: "0.
|
|
235
|
+
clientInfo: { name: "bazis", title: "Bazis", version: "0.103.0" }, capabilities: { experimentalApi: true },
|
|
236
236
|
}, 15_000, signal);
|
|
237
237
|
if (resolve(text(initialized.codexHome, 4096)) !== this.paths.home) throw new CodexError("PROTOCOL_ERROR");
|
|
238
238
|
client.notify("initialized");
|
|
@@ -5,12 +5,18 @@ import { reader, requireValue } from "../connectorConfig";
|
|
|
5
5
|
import { CodexAppServerClient } from "./codex/CodexAppServerClient";
|
|
6
6
|
import type { CodexClient, CodexConfigShape } from "./codex/contracts";
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Codex App Server client of the `codexAppServerConnect()` connector,
|
|
10
|
+
* injected by type like the other infra clients:
|
|
11
|
+
* `constructor(private readonly codex: ICodex)`.
|
|
12
|
+
*/
|
|
13
|
+
export interface ICodex extends CodexClient {}
|
|
14
|
+
export const ICodex = createToken<ICodex>("ICodex") as InjectionToken<CodexClient>;
|
|
9
15
|
|
|
10
16
|
/** App Server is a full executor; it does not implement the raw AgentModelProvider port. */
|
|
11
17
|
export function codexAppServerConnect(config: AppConfig<CodexConfigShape>): InfraConnector<CodexClient> {
|
|
12
18
|
return {
|
|
13
|
-
token:
|
|
19
|
+
token: ICodex, config, phase: 0,
|
|
14
20
|
create(configs) {
|
|
15
21
|
const view = reader(config, configs);
|
|
16
22
|
return new CodexAppServerClient({
|
package/core/infra/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { Infra, infraModule, type InfraManifest } from "./Infra";
|
|
|
7
7
|
export { InfraError, errorMessage, type ConnectionDescription, type InfraConnector } from "./InfraConnector";
|
|
8
8
|
export { InfraLifecycle } from "./InfraLifecycle";
|
|
9
9
|
export { reader, requireValue, settingNames, type ConfigReader } from "./connectorConfig";
|
|
10
|
-
export { codexAppServerConnect,
|
|
10
|
+
export { codexAppServerConnect, ICodex } from "./connectors/codex";
|
|
11
11
|
export { CodexError, RU_CODEX_MESSAGES, type CodexErrorCode, CODEX_MODEL_ID, CODEX_REASONING_EFFORT, type CodexClient, type CodexConfigShape, type CodexLogin, type CodexLoginMethod, type CodexModel, type CodexStatus, type CodexRunInput, type CodexChatMessage, type CodexTool, type CodexToolCall, type CodexToolResult } from "./connectors/codex/contracts";
|
|
12
12
|
|
|
13
13
|
export { postgres, IPostgres, postgresConnectionOptions, type PostgresConfigShape, type PostgresSslMode, type PostgresConnectorOptions } from "./connectors/postgres";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bazis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.103.0",
|
|
4
4
|
"description": "Modular backend framework for Bun in TypeScript: DI wired by codegen, HTTP, a PostgreSQL ORM, JWT, WebSocket, agents and a CLI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alexander Alenin",
|