bazis 0.98.0 → 0.98.1
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/http/Binding/ParameterBinder.ts +16 -1
- package/core/http/Binding/autoBindings.ts +1 -1
- package/core/http/Binding/bindings.ts +2 -0
- package/core/http/Binding/generatedSpec.ts +2 -0
- package/core/http/OpenApi/openApiDocument.ts +2 -2
- package/core/http/Routing/Router.ts +14 -1
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/scripts/di-generate.ts +18 -0
- package/package.json +1 -1
|
@@ -24,6 +24,21 @@ function bindValue(binding: ParameterBinding, raw: string | undefined, kind: str
|
|
|
24
24
|
return binding.type && binding.type !== "string" ? convertOr400(raw, binding.type, binding.name ?? "?") : raw;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* `?tag=a&tag=b` → `["a", "b"]`, each value converted by the element type.
|
|
29
|
+
* Without the parameter: the default, `undefined` for `tag?: string[]`, or `[]`.
|
|
30
|
+
*/
|
|
31
|
+
function bindArray(binding: ParameterBinding, ctx: HttpContext): unknown {
|
|
32
|
+
const raw = ctx.url.searchParams.getAll(binding.name ?? "");
|
|
33
|
+
if (raw.length === 0) {
|
|
34
|
+
if (binding.defaultValue !== undefined) return binding.defaultValue;
|
|
35
|
+
return binding.optional ? undefined : [];
|
|
36
|
+
}
|
|
37
|
+
return binding.type && binding.type !== "string"
|
|
38
|
+
? raw.map((value) => convertOr400(value, binding.type!, binding.name ?? "?"))
|
|
39
|
+
: raw;
|
|
40
|
+
}
|
|
41
|
+
|
|
27
42
|
/**
|
|
28
43
|
* Resolves generated binding descriptors into the action's argument list.
|
|
29
44
|
* All conversions fail with 400 (never 500); body parse errors and model
|
|
@@ -52,7 +67,7 @@ export async function bindArguments(
|
|
|
52
67
|
break;
|
|
53
68
|
}
|
|
54
69
|
case "query":
|
|
55
|
-
args[index] = bindValue(binding, ctx.query(binding.name ?? ""), "query");
|
|
70
|
+
args[index] = binding.array ? bindArray(binding, ctx) : bindValue(binding, ctx.query(binding.name ?? ""), "query");
|
|
56
71
|
break;
|
|
57
72
|
case "body": {
|
|
58
73
|
const contentType = ctx.header("content-type") ?? "";
|
|
@@ -55,7 +55,7 @@ function toBinding(spec: GeneratedBindingSpec, controllerName: string, methodNam
|
|
|
55
55
|
case "route":
|
|
56
56
|
return { source: "route", name: spec.name, type: spec.type, optional: spec.optional };
|
|
57
57
|
case "query":
|
|
58
|
-
return { source: "query", name: spec.name, type: spec.type, optional: spec.optional };
|
|
58
|
+
return { source: "query", name: spec.name, type: spec.type, optional: spec.optional, array: spec.array };
|
|
59
59
|
case "context":
|
|
60
60
|
return { source: "context" };
|
|
61
61
|
case "request":
|
|
@@ -11,6 +11,8 @@ export interface ParameterBinding {
|
|
|
11
11
|
readonly type?: ValueType;
|
|
12
12
|
readonly defaultValue?: unknown;
|
|
13
13
|
readonly optional?: boolean;
|
|
14
|
+
/** Query array: all values of the name (`?tag=a&tag=b`), each converted by `type`. */
|
|
15
|
+
readonly array?: boolean;
|
|
14
16
|
/** Body model class (instantiated, sanitized and validated). */
|
|
15
17
|
readonly model?: Class<object>;
|
|
16
18
|
/** Whitelist/limits for the class-based list binding (JSON:API list query). */
|
|
@@ -13,6 +13,8 @@ export interface GeneratedBindingSpec {
|
|
|
13
13
|
readonly type?: "int" | "number" | "bool" | "string";
|
|
14
14
|
/** The parameter is declared with `?` or a default value. */
|
|
15
15
|
readonly optional?: boolean;
|
|
16
|
+
/** A query array (`tag: string[]`): every `?tag=` value, each converted by `type`. */
|
|
17
|
+
readonly array?: boolean;
|
|
16
18
|
/** DTO class name (resolved through the generated target; `@RequestModel()` classes are the fallback). */
|
|
17
19
|
readonly model?: string;
|
|
18
20
|
}
|
|
@@ -173,8 +173,8 @@ function collectParameters(
|
|
|
173
173
|
parameters.push({
|
|
174
174
|
name: binding.name,
|
|
175
175
|
in: "query",
|
|
176
|
-
required: binding.optional !== true && binding.defaultValue === undefined,
|
|
177
|
-
schema: schemaForValueType(binding.type),
|
|
176
|
+
required: binding.array !== true && binding.optional !== true && binding.defaultValue === undefined,
|
|
177
|
+
schema: binding.array ? { type: "array", items: schemaForValueType(binding.type) } : schemaForValueType(binding.type),
|
|
178
178
|
});
|
|
179
179
|
}
|
|
180
180
|
break;
|
|
@@ -17,6 +17,8 @@ export interface RouteAction {
|
|
|
17
17
|
|
|
18
18
|
interface RouteEntry {
|
|
19
19
|
readonly action: RouteAction;
|
|
20
|
+
/** The registered path, for diagnostics: `/tasks/:id`. */
|
|
21
|
+
readonly template: string;
|
|
20
22
|
/** Names belong to the selected declaration, never to shared tree edges. */
|
|
21
23
|
readonly bindings: readonly { readonly name: string; readonly index: number }[];
|
|
22
24
|
}
|
|
@@ -58,6 +60,7 @@ export class Router {
|
|
|
58
60
|
register(segments: readonly TemplateSegment[], httpMethod: string, versionKey: string, action: RouteAction): void {
|
|
59
61
|
const entry: RouteEntry = {
|
|
60
62
|
action,
|
|
63
|
+
template: renderTemplate(segments),
|
|
61
64
|
bindings: segments.flatMap((segment, index) => segment.kind === "static" ? [] : [{ name: segment.name, index }]),
|
|
62
65
|
};
|
|
63
66
|
let node = this.root;
|
|
@@ -101,8 +104,11 @@ export class Router {
|
|
|
101
104
|
leaf.set(httpMethod, versions);
|
|
102
105
|
}
|
|
103
106
|
if (versions.has(versionKey)) {
|
|
107
|
+
const existing = versions.get(versionKey)!;
|
|
108
|
+
const version = versionKey ? ` (version ${versionKey})` : "";
|
|
104
109
|
throw new HttpSetupError(
|
|
105
|
-
`Duplicate route: ${httpMethod}
|
|
110
|
+
`Duplicate route: ${httpMethod} ${entry.template}${version} is mapped to both ${existing.action.name} (${existing.template}) and ${entry.action.name}. `
|
|
111
|
+
+ "Routes that differ only in parameter names are the same route; change one of the paths.",
|
|
106
112
|
);
|
|
107
113
|
}
|
|
108
114
|
versions.set(versionKey, entry);
|
|
@@ -199,3 +205,10 @@ function constraintPriority(constraint: SegmentConstraint | undefined): number {
|
|
|
199
205
|
return 0;
|
|
200
206
|
}
|
|
201
207
|
}
|
|
208
|
+
|
|
209
|
+
/** `/tasks/:id(int)/files/*path` from parsed segments. */
|
|
210
|
+
function renderTemplate(segments: readonly TemplateSegment[]): string {
|
|
211
|
+
return `/${segments.map((segment) => segment.kind === "static" ? segment.value
|
|
212
|
+
: segment.kind === "param" ? `:${segment.name}${segment.constraint ? `(${segment.constraint.name})` : ""}`
|
|
213
|
+
: `*${segment.name}`).join("/")}`;
|
|
214
|
+
}
|
|
@@ -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.98.
|
|
235
|
+
clientInfo: { name: "bazis", title: "Bazis", version: "0.98.1" }, 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");
|
|
@@ -178,6 +178,7 @@ interface HttpBindingSpec {
|
|
|
178
178
|
type?: "int" | "number" | "bool" | "string";
|
|
179
179
|
optional?: boolean;
|
|
180
180
|
model?: string;
|
|
181
|
+
array?: boolean;
|
|
181
182
|
}
|
|
182
183
|
let httpBindings: Record<string, Record<string, HttpBindingSpec[]>> = {};
|
|
183
184
|
interface GrpcRequestBinding {
|
|
@@ -1030,6 +1031,16 @@ function readConstructorTypeNames(node: ts.ClassDeclaration): ConstructorDepende
|
|
|
1030
1031
|
}
|
|
1031
1032
|
|
|
1032
1033
|
/** Resolve imported type aliases/re-exports; runtime named binding stays module-scoped. */
|
|
1034
|
+
/** The element keyword of an array type: `string[]`, `Array<string>`, `readonly string[]` -> "string". */
|
|
1035
|
+
function arrayElementKeyword(node: ts.TypeNode): string | undefined {
|
|
1036
|
+
if (ts.isTypeOperatorNode(node) && node.operator === ts.SyntaxKind.ReadonlyKeyword) return arrayElementKeyword(node.type);
|
|
1037
|
+
if (ts.isArrayTypeNode(node)) return node.elementType.getText().trim();
|
|
1038
|
+
if (ts.isTypeReferenceNode(node) && ["Array", "ReadonlyArray"].includes(node.typeName.getText()) && node.typeArguments?.length === 1) {
|
|
1039
|
+
return node.typeArguments[0]!.getText().trim();
|
|
1040
|
+
}
|
|
1041
|
+
return undefined;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1033
1044
|
function getDependencyTypeName(node: ts.TypeNode): string | undefined {
|
|
1034
1045
|
if (!ts.isTypeReferenceNode(node)) return undefined;
|
|
1035
1046
|
const symbol = checker.getSymbolAtLocation(node.typeName);
|
|
@@ -1425,6 +1436,13 @@ function collectControllerBindings(node: ts.ClassDeclaration, controllerName: st
|
|
|
1425
1436
|
specs.push({ source: "route", name: paramName, type: type === "string" ? undefined : type, optional });
|
|
1426
1437
|
continue;
|
|
1427
1438
|
}
|
|
1439
|
+
// `tag: string[]`, `Array<number>`, `readonly boolean[]` -> every ?tag= value.
|
|
1440
|
+
const elementKeyword = typeNode ? arrayElementKeyword(typeNode) : undefined;
|
|
1441
|
+
const elementType = elementKeyword !== undefined ? PRIMITIVE_QUERY_TYPES[elementKeyword] : undefined;
|
|
1442
|
+
if (elementType !== undefined) {
|
|
1443
|
+
specs.push({ source: "query", name: paramName, type: elementType === "string" ? undefined : elementType, optional, array: true });
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1428
1446
|
const primitiveType =
|
|
1429
1447
|
keywordType !== undefined ? PRIMITIVE_QUERY_TYPES[keywordType] : inferTypeFromInitializer(param.initializer);
|
|
1430
1448
|
if (primitiveType !== undefined) {
|
package/package.json
CHANGED