@typed/app 1.0.0-beta.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/README.md +166 -0
- package/dist/HttpApiVirtualModulePlugin.d.ts +26 -0
- package/dist/HttpApiVirtualModulePlugin.d.ts.map +1 -0
- package/dist/HttpApiVirtualModulePlugin.js +301 -0
- package/dist/RouterVirtualModulePlugin.d.ts +23 -0
- package/dist/RouterVirtualModulePlugin.d.ts.map +1 -0
- package/dist/RouterVirtualModulePlugin.js +176 -0
- package/dist/createTypeInfoApiSessionForApp.d.ts +29 -0
- package/dist/createTypeInfoApiSessionForApp.d.ts.map +1 -0
- package/dist/createTypeInfoApiSessionForApp.js +46 -0
- package/dist/httpapi/defineApiHandler.d.ts +70 -0
- package/dist/httpapi/defineApiHandler.d.ts.map +1 -0
- package/dist/httpapi/defineApiHandler.js +23 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/internal/appConfigTypes.d.ts +11 -0
- package/dist/internal/appConfigTypes.d.ts.map +1 -0
- package/dist/internal/appConfigTypes.js +1 -0
- package/dist/internal/appLayerTypes.d.ts +24 -0
- package/dist/internal/appLayerTypes.d.ts.map +1 -0
- package/dist/internal/appLayerTypes.js +28 -0
- package/dist/internal/buildRouteDescriptors.d.ts +48 -0
- package/dist/internal/buildRouteDescriptors.d.ts.map +1 -0
- package/dist/internal/buildRouteDescriptors.js +371 -0
- package/dist/internal/emitHttpApiSource.d.ts +18 -0
- package/dist/internal/emitHttpApiSource.d.ts.map +1 -0
- package/dist/internal/emitHttpApiSource.js +404 -0
- package/dist/internal/emitRouterHelpers.d.ts +17 -0
- package/dist/internal/emitRouterHelpers.d.ts.map +1 -0
- package/dist/internal/emitRouterHelpers.js +74 -0
- package/dist/internal/emitRouterSource.d.ts +8 -0
- package/dist/internal/emitRouterSource.d.ts.map +1 -0
- package/dist/internal/emitRouterSource.js +139 -0
- package/dist/internal/extractHttpApiLiterals.d.ts +17 -0
- package/dist/internal/extractHttpApiLiterals.d.ts.map +1 -0
- package/dist/internal/extractHttpApiLiterals.js +45 -0
- package/dist/internal/httpapiDescriptorTree.d.ts +75 -0
- package/dist/internal/httpapiDescriptorTree.d.ts.map +1 -0
- package/dist/internal/httpapiDescriptorTree.js +182 -0
- package/dist/internal/httpapiEndpointContract.d.ts +32 -0
- package/dist/internal/httpapiEndpointContract.d.ts.map +1 -0
- package/dist/internal/httpapiEndpointContract.js +79 -0
- package/dist/internal/httpapiFileRoles.d.ts +67 -0
- package/dist/internal/httpapiFileRoles.d.ts.map +1 -0
- package/dist/internal/httpapiFileRoles.js +145 -0
- package/dist/internal/httpapiId.d.ts +30 -0
- package/dist/internal/httpapiId.d.ts.map +1 -0
- package/dist/internal/httpapiId.js +57 -0
- package/dist/internal/httpapiOpenApiConfig.d.ts +87 -0
- package/dist/internal/httpapiOpenApiConfig.d.ts.map +1 -0
- package/dist/internal/httpapiOpenApiConfig.js +144 -0
- package/dist/internal/httpapiSort.d.ts +16 -0
- package/dist/internal/httpapiSort.d.ts.map +1 -0
- package/dist/internal/httpapiSort.js +29 -0
- package/dist/internal/path.d.ts +16 -0
- package/dist/internal/path.d.ts.map +1 -0
- package/dist/internal/path.js +38 -0
- package/dist/internal/resolveConfig.d.ts +8 -0
- package/dist/internal/resolveConfig.d.ts.map +1 -0
- package/dist/internal/resolveConfig.js +13 -0
- package/dist/internal/routeIdentifiers.d.ts +18 -0
- package/dist/internal/routeIdentifiers.d.ts.map +1 -0
- package/dist/internal/routeIdentifiers.js +90 -0
- package/dist/internal/routeTypeNode.d.ts +45 -0
- package/dist/internal/routeTypeNode.d.ts.map +1 -0
- package/dist/internal/routeTypeNode.js +93 -0
- package/dist/internal/routerDescriptorTree.d.ts +110 -0
- package/dist/internal/routerDescriptorTree.d.ts.map +1 -0
- package/dist/internal/routerDescriptorTree.js +230 -0
- package/dist/internal/typeTargetBootstrap.d.ts +2 -0
- package/dist/internal/typeTargetBootstrap.d.ts.map +1 -0
- package/dist/internal/typeTargetBootstrap.js +23 -0
- package/dist/internal/typeTargetBootstrapHttpApi.d.ts +2 -0
- package/dist/internal/typeTargetBootstrapHttpApi.d.ts.map +1 -0
- package/dist/internal/typeTargetBootstrapHttpApi.js +21 -0
- package/dist/internal/typeTargetSpecs.d.ts +15 -0
- package/dist/internal/typeTargetSpecs.d.ts.map +1 -0
- package/dist/internal/typeTargetSpecs.js +32 -0
- package/dist/internal/validation.d.ts +12 -0
- package/dist/internal/validation.d.ts.map +1 -0
- package/dist/internal/validation.js +32 -0
- package/package.json +45 -0
- package/src/HttpApiVirtualModulePlugin.test.ts +1062 -0
- package/src/HttpApiVirtualModulePlugin.ts +376 -0
- package/src/RouterVirtualModulePlugin.test.ts +1254 -0
- package/src/RouterVirtualModulePlugin.ts +242 -0
- package/src/createTypeInfoApiSessionForApp.ts +57 -0
- package/src/defineApiHandler.test.ts +100 -0
- package/src/httpapi/defineApiHandler.ts +141 -0
- package/src/httpapiDescriptorTree.test.ts +124 -0
- package/src/httpapiEndpointContract.test.ts +160 -0
- package/src/httpapiFileRoles.test.ts +105 -0
- package/src/index.ts +40 -0
- package/src/internal/appConfigTypes.ts +12 -0
- package/src/internal/appLayerTypes.ts +79 -0
- package/src/internal/buildRouteDescriptors.ts +489 -0
- package/src/internal/emitHttpApiSource.ts +563 -0
- package/src/internal/emitRouterHelpers.ts +89 -0
- package/src/internal/emitRouterSource.ts +191 -0
- package/src/internal/extractHttpApiLiterals.ts +67 -0
- package/src/internal/httpapiDescriptorTree.ts +283 -0
- package/src/internal/httpapiEndpointContract.ts +110 -0
- package/src/internal/httpapiFileRoles.ts +204 -0
- package/src/internal/httpapiId.ts +78 -0
- package/src/internal/httpapiOpenApiConfig.ts +228 -0
- package/src/internal/httpapiSort.ts +39 -0
- package/src/internal/path.ts +46 -0
- package/src/internal/resolveConfig.ts +15 -0
- package/src/internal/routeIdentifiers.ts +93 -0
- package/src/internal/routeTypeNode.ts +120 -0
- package/src/internal/routerDescriptorTree.ts +366 -0
- package/src/internal/typeTargetBootstrap.ts +24 -0
- package/src/internal/typeTargetBootstrapHttpApi.ts +22 -0
- package/src/internal/typeTargetSpecs.ts +35 -0
- package/src/internal/validation.ts +46 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for HttpApi endpoint contract validation.
|
|
3
|
+
* @see .docs/specs/httpapi-virtual-module-plugin/testing-strategy.md (endpoint contract validation)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import {
|
|
8
|
+
validateEndpointContract,
|
|
9
|
+
type EndpointContractInput,
|
|
10
|
+
} from "./internal/httpapiEndpointContract.js";
|
|
11
|
+
|
|
12
|
+
describe("validateEndpointContract", () => {
|
|
13
|
+
const validContract: EndpointContractInput = {
|
|
14
|
+
route: { path: "/users", pathSchema: {}, querySchema: {} },
|
|
15
|
+
method: "GET",
|
|
16
|
+
handler: () => {},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
it("accepts contract with all required fields and valid method", () => {
|
|
20
|
+
const result = validateEndpointContract(validContract);
|
|
21
|
+
expect(result.ok).toBe(true);
|
|
22
|
+
if (result.ok) {
|
|
23
|
+
expect(result.value.method).toBe("GET");
|
|
24
|
+
expect(result.value.route).toBe(validContract.route);
|
|
25
|
+
expect(result.value.handler).toBe(validContract.handler);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("accepts all supported HTTP methods", () => {
|
|
30
|
+
const methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
31
|
+
for (const method of methods) {
|
|
32
|
+
const result = validateEndpointContract({
|
|
33
|
+
...validContract,
|
|
34
|
+
method,
|
|
35
|
+
});
|
|
36
|
+
expect(result.ok, `method ${method}`).toBe(true);
|
|
37
|
+
if (result.ok) expect(result.value.method).toBe(method);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("rejects non-object input", () => {
|
|
42
|
+
expect(validateEndpointContract(null)).toMatchInlineSnapshot(`
|
|
43
|
+
{
|
|
44
|
+
"ok": false,
|
|
45
|
+
"reason": "Endpoint contract must be an object",
|
|
46
|
+
}
|
|
47
|
+
`);
|
|
48
|
+
expect(validateEndpointContract(undefined)).toMatchInlineSnapshot(`
|
|
49
|
+
{
|
|
50
|
+
"ok": false,
|
|
51
|
+
"reason": "Endpoint contract must be an object",
|
|
52
|
+
}
|
|
53
|
+
`);
|
|
54
|
+
expect(validateEndpointContract("string")).toMatchInlineSnapshot(`
|
|
55
|
+
{
|
|
56
|
+
"ok": false,
|
|
57
|
+
"reason": "Endpoint contract must be an object",
|
|
58
|
+
}
|
|
59
|
+
`);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("rejects missing route", () => {
|
|
63
|
+
const { route: _, ...noRoute } = validContract as Record<string, unknown>;
|
|
64
|
+
expect(validateEndpointContract(noRoute)).toMatchInlineSnapshot(`
|
|
65
|
+
{
|
|
66
|
+
"ok": false,
|
|
67
|
+
"reason": "Endpoint contract missing required field: route",
|
|
68
|
+
}
|
|
69
|
+
`);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("rejects missing method", () => {
|
|
73
|
+
const { method: _, ...noMethod } = validContract as Record<string, unknown>;
|
|
74
|
+
expect(validateEndpointContract(noMethod)).toMatchInlineSnapshot(`
|
|
75
|
+
{
|
|
76
|
+
"ok": false,
|
|
77
|
+
"reason": "Endpoint contract method: method must be a string",
|
|
78
|
+
}
|
|
79
|
+
`);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("rejects invalid method", () => {
|
|
83
|
+
expect(
|
|
84
|
+
validateEndpointContract({ ...validContract, method: "INVALID" }),
|
|
85
|
+
).toMatchInlineSnapshot(`
|
|
86
|
+
{
|
|
87
|
+
"ok": false,
|
|
88
|
+
"reason": "Endpoint contract method must be one of: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS",
|
|
89
|
+
}
|
|
90
|
+
`);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("rejects empty string method", () => {
|
|
94
|
+
const result = validateEndpointContract({
|
|
95
|
+
...validContract,
|
|
96
|
+
method: " ",
|
|
97
|
+
});
|
|
98
|
+
expect(result.ok).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("rejects route that is not an object", () => {
|
|
102
|
+
expect(
|
|
103
|
+
validateEndpointContract({ ...validContract, route: null }),
|
|
104
|
+
).toMatchInlineSnapshot(`
|
|
105
|
+
{
|
|
106
|
+
"ok": false,
|
|
107
|
+
"reason": "Endpoint contract route must be an object (path + pathSchema + querySchema)",
|
|
108
|
+
}
|
|
109
|
+
`);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("rejects route missing pathSchema", () => {
|
|
113
|
+
expect(
|
|
114
|
+
validateEndpointContract({
|
|
115
|
+
...validContract,
|
|
116
|
+
route: { path: "/x", querySchema: {} },
|
|
117
|
+
}),
|
|
118
|
+
).toMatchInlineSnapshot(`
|
|
119
|
+
{
|
|
120
|
+
"ok": false,
|
|
121
|
+
"reason": "Endpoint contract route must include pathSchema",
|
|
122
|
+
}
|
|
123
|
+
`);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("rejects route missing querySchema", () => {
|
|
127
|
+
expect(
|
|
128
|
+
validateEndpointContract({
|
|
129
|
+
...validContract,
|
|
130
|
+
route: { path: "/x", pathSchema: {} },
|
|
131
|
+
}),
|
|
132
|
+
).toMatchInlineSnapshot(`
|
|
133
|
+
{
|
|
134
|
+
"ok": false,
|
|
135
|
+
"reason": "Endpoint contract route must include querySchema",
|
|
136
|
+
}
|
|
137
|
+
`);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("rejects missing handler", () => {
|
|
141
|
+
const { handler: _, ...noHandler } = validContract as Record<string, unknown>;
|
|
142
|
+
expect(validateEndpointContract(noHandler)).toMatchInlineSnapshot(`
|
|
143
|
+
{
|
|
144
|
+
"ok": false,
|
|
145
|
+
"reason": "Endpoint contract missing required field: handler",
|
|
146
|
+
}
|
|
147
|
+
`);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("rejects handler that is not a function", () => {
|
|
151
|
+
expect(
|
|
152
|
+
validateEndpointContract({ ...validContract, handler: "not a function" }),
|
|
153
|
+
).toMatchInlineSnapshot(`
|
|
154
|
+
{
|
|
155
|
+
"ok": false,
|
|
156
|
+
"reason": "Endpoint contract handler must be a function",
|
|
157
|
+
}
|
|
158
|
+
`);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
classifyHttpApiFileRole,
|
|
4
|
+
compareHttpApiPathOrder,
|
|
5
|
+
HTTPAPI_DIRECTORY_COMPANION_FILES,
|
|
6
|
+
HTTPAPI_ENDPOINT_COMPANION_SUFFIXES,
|
|
7
|
+
isHttpApiScriptExtension,
|
|
8
|
+
normalizeHttpApiRelativePath,
|
|
9
|
+
sortHttpApiPaths,
|
|
10
|
+
} from "./internal/httpapiFileRoles.js";
|
|
11
|
+
|
|
12
|
+
describe("httpapiFileRoles", () => {
|
|
13
|
+
describe("normalizeHttpApiRelativePath", () => {
|
|
14
|
+
it("strips leading ./ and normalizes slashes", () => {
|
|
15
|
+
expect(normalizeHttpApiRelativePath("./users/list.ts")).toBe("users/list.ts");
|
|
16
|
+
expect(normalizeHttpApiRelativePath("users\\list.ts")).toBe("users/list.ts");
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("isHttpApiScriptExtension", () => {
|
|
21
|
+
it("accepts .ts, .tsx, .js, .jsx, .mts, .cts, .mjs, .cjs", () => {
|
|
22
|
+
expect(isHttpApiScriptExtension(".ts")).toBe(true);
|
|
23
|
+
expect(isHttpApiScriptExtension(".TS")).toBe(true);
|
|
24
|
+
expect(isHttpApiScriptExtension(".js")).toBe(true);
|
|
25
|
+
expect(isHttpApiScriptExtension(".mts")).toBe(true);
|
|
26
|
+
expect(isHttpApiScriptExtension(".cjs")).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
it("rejects .d.ts and others", () => {
|
|
29
|
+
expect(isHttpApiScriptExtension(".d.ts")).toBe(false);
|
|
30
|
+
expect(isHttpApiScriptExtension(".json")).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("classifyHttpApiFileRole", () => {
|
|
35
|
+
it("classifies _api.ts at root as api_root", () => {
|
|
36
|
+
const r = classifyHttpApiFileRole("_api.ts");
|
|
37
|
+
expect(r).toEqual({ role: "api_root", path: "_api.ts" });
|
|
38
|
+
});
|
|
39
|
+
it("classifies _api.ts in subdir as unsupported_reserved", () => {
|
|
40
|
+
const r = classifyHttpApiFileRole("users/_api.ts");
|
|
41
|
+
expect(r.role).toBe("unsupported_reserved");
|
|
42
|
+
expect("diagnosticCode" in r && r.diagnosticCode).toBe("HTTPAPI-ROLE-004");
|
|
43
|
+
});
|
|
44
|
+
it("classifies _group.ts as group_override", () => {
|
|
45
|
+
expect(classifyHttpApiFileRole("_group.ts")).toEqual({
|
|
46
|
+
role: "group_override",
|
|
47
|
+
path: "_group.ts",
|
|
48
|
+
});
|
|
49
|
+
expect(classifyHttpApiFileRole("users/_group.ts")).toEqual({
|
|
50
|
+
role: "group_override",
|
|
51
|
+
path: "users/_group.ts",
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
it("classifies directory companions", () => {
|
|
55
|
+
for (const f of HTTPAPI_DIRECTORY_COMPANION_FILES) {
|
|
56
|
+
const r = classifyHttpApiFileRole(`users/${f}`);
|
|
57
|
+
expect(r.role).toBe("directory_companion");
|
|
58
|
+
expect("kind" in r && r.kind).toBe(f);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
it("classifies endpoint primary (no companion suffix)", () => {
|
|
62
|
+
const r = classifyHttpApiFileRole("users/list.ts");
|
|
63
|
+
expect(r).toEqual({ role: "endpoint_primary", path: "users/list.ts" });
|
|
64
|
+
});
|
|
65
|
+
it("classifies endpoint companions", () => {
|
|
66
|
+
for (const suffix of HTTPAPI_ENDPOINT_COMPANION_SUFFIXES) {
|
|
67
|
+
const r = classifyHttpApiFileRole(`users/list${suffix}.ts`);
|
|
68
|
+
expect(r.role).toBe("endpoint_companion");
|
|
69
|
+
expect("kind" in r && r.kind).toBe(suffix);
|
|
70
|
+
expect("endpointStem" in r && r.endpointStem).toBe("list");
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
it("rejects .d.ts as unsupported_reserved", () => {
|
|
74
|
+
const r = classifyHttpApiFileRole("list.d.ts");
|
|
75
|
+
expect(r.role).toBe("unsupported_reserved");
|
|
76
|
+
expect("diagnosticCode" in r && r.diagnosticCode).toBe("HTTPAPI-ROLE-001");
|
|
77
|
+
});
|
|
78
|
+
it("rejects underscore-prefixed non-matrix files as unsupported_reserved", () => {
|
|
79
|
+
const r = classifyHttpApiFileRole("_unknown.ts");
|
|
80
|
+
expect(r.role).toBe("unsupported_reserved");
|
|
81
|
+
expect("diagnosticCode" in r && r.diagnosticCode).toBe("HTTPAPI-ROLE-006");
|
|
82
|
+
});
|
|
83
|
+
it("rejects endpoint companion with empty base name", () => {
|
|
84
|
+
const r = classifyHttpApiFileRole(".openapi.ts");
|
|
85
|
+
expect(r.role).toBe("unsupported_reserved");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("sortHttpApiPaths / compareHttpApiPathOrder", () => {
|
|
90
|
+
it("sorts deterministically by normalized path", () => {
|
|
91
|
+
const paths = ["z/endpoint.ts", "a/endpoint.ts", "users/list.ts", "_api.ts"];
|
|
92
|
+
expect(sortHttpApiPaths(paths)).toEqual([
|
|
93
|
+
"_api.ts",
|
|
94
|
+
"a/endpoint.ts",
|
|
95
|
+
"users/list.ts",
|
|
96
|
+
"z/endpoint.ts",
|
|
97
|
+
]);
|
|
98
|
+
});
|
|
99
|
+
it("compareHttpApiPathOrder is stable", () => {
|
|
100
|
+
expect(compareHttpApiPathOrder("a", "b")).toBeLessThan(0);
|
|
101
|
+
expect(compareHttpApiPathOrder("b", "a")).toBeGreaterThan(0);
|
|
102
|
+
expect(compareHttpApiPathOrder("a", "a")).toBe(0);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createRouterVirtualModulePlugin,
|
|
3
|
+
parseRouterVirtualModuleId,
|
|
4
|
+
resolveRouterTargetDirectory,
|
|
5
|
+
type ParseRouterVirtualModuleIdResult,
|
|
6
|
+
type ResolveRouterTargetDirectoryResult,
|
|
7
|
+
type RouterVirtualModulePluginOptions,
|
|
8
|
+
} from "./RouterVirtualModulePlugin.js";
|
|
9
|
+
export {
|
|
10
|
+
ROUTER_TYPE_TARGET_SPECS,
|
|
11
|
+
HTTPAPI_TYPE_TARGET_SPECS,
|
|
12
|
+
} from "./internal/typeTargetSpecs.js";
|
|
13
|
+
export {
|
|
14
|
+
createTypeInfoApiSessionForApp,
|
|
15
|
+
APP_TYPE_TARGET_BOOTSTRAP_CONTENT,
|
|
16
|
+
} from "./createTypeInfoApiSessionForApp.js";
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
createHttpApiVirtualModulePlugin,
|
|
20
|
+
parseHttpApiVirtualModuleId,
|
|
21
|
+
resolveHttpApiTargetDirectory,
|
|
22
|
+
type HttpApiVirtualModulePluginOptions,
|
|
23
|
+
type ParseHttpApiVirtualModuleIdResult,
|
|
24
|
+
type ResolveHttpApiTargetDirectoryResult,
|
|
25
|
+
} from "./HttpApiVirtualModulePlugin.js";
|
|
26
|
+
export {
|
|
27
|
+
defineApiHandler,
|
|
28
|
+
emptyRecordString,
|
|
29
|
+
emptyRecordStringArray,
|
|
30
|
+
type ApiHandlerContext,
|
|
31
|
+
type ApiHandlerFn,
|
|
32
|
+
type ApiHandlerParams,
|
|
33
|
+
type ApiRoute,
|
|
34
|
+
type EndpointSchemas,
|
|
35
|
+
type HttpMethod,
|
|
36
|
+
type TypedApiHandler,
|
|
37
|
+
} from "./httpapi/defineApiHandler.js";
|
|
38
|
+
export { resolveConfig } from "./internal/resolveConfig.js";
|
|
39
|
+
export type { AppConfig, RunConfig } from "./internal/appConfigTypes.js";
|
|
40
|
+
export * from "./internal/appLayerTypes.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type * as Config from "effect/Config";
|
|
2
|
+
|
|
3
|
+
/** Config for App (sync). disableListenLog matches HttpRouter.serve option. Use raw values. */
|
|
4
|
+
export type AppConfig = {
|
|
5
|
+
readonly disableListenLog?: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/** Config for run(). Extends AppConfig; host/port may be raw or Config (yield* Config.*). Supply ConfigProvider.layer when using Config. */
|
|
9
|
+
export type RunConfig = AppConfig & {
|
|
10
|
+
readonly host?: string | Config.Config<string>;
|
|
11
|
+
readonly port?: number | Config.Config<number>;
|
|
12
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-safe Layer composition helpers for App/run.
|
|
3
|
+
* Uses Effect's Layer.Success, Layer.Error, Layer.Services, Layer.Any directly.
|
|
4
|
+
*/
|
|
5
|
+
import * as Layer from "effect/Layer";
|
|
6
|
+
|
|
7
|
+
export type LayerAny = Layer.Layer<any, any, any> | Layer.Layer<never, any, any>;
|
|
8
|
+
|
|
9
|
+
/** Single layer or non-empty array (uses Layer.mergeAll internally). */
|
|
10
|
+
export type LayerOrGroup = LayerAny | readonly [LayerAny, ...ReadonlyArray<LayerAny>];
|
|
11
|
+
|
|
12
|
+
function isLayerArray(l: LayerOrGroup): l is readonly [LayerAny, ...ReadonlyArray<LayerAny>] {
|
|
13
|
+
return Array.isArray(l) && l.length > 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function toLayer(l: LayerOrGroup): LayerAny {
|
|
17
|
+
if (isLayerArray(l)) {
|
|
18
|
+
const [first, ...rest] = l;
|
|
19
|
+
return Layer.mergeAll(first, ...rest);
|
|
20
|
+
}
|
|
21
|
+
return l;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Composes base layer with layers via provideMerge. Layers are provided TO the base.
|
|
26
|
+
* For precise output type, use ComposeWithLayers<Base, NormalizedLayers<Layers>>.
|
|
27
|
+
*/
|
|
28
|
+
export function composeWithLayers<
|
|
29
|
+
Base extends LayerAny,
|
|
30
|
+
const Layers extends ReadonlyArray<LayerOrGroup>,
|
|
31
|
+
>(base: Base, layers: Layers): ComputeLayers<Layers, Base>;
|
|
32
|
+
export function composeWithLayers<
|
|
33
|
+
Base extends LayerAny,
|
|
34
|
+
const Layers extends ReadonlyArray<LayerOrGroup>,
|
|
35
|
+
>(base: Base, layers: Layers): LayerAny {
|
|
36
|
+
if (layers.length === 0) return base;
|
|
37
|
+
let out: LayerAny = base;
|
|
38
|
+
for (const layer of layers) {
|
|
39
|
+
out = Layer.provideMerge(out, toLayer(layer));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type ComputeLayers<
|
|
46
|
+
Layers extends ReadonlyArray<LayerOrGroup>,
|
|
47
|
+
R extends LayerAny,
|
|
48
|
+
> = readonly [] extends Layers
|
|
49
|
+
? R
|
|
50
|
+
: Layers extends readonly [
|
|
51
|
+
infer Head extends LayerOrGroup,
|
|
52
|
+
...infer Tail extends ReadonlyArray<LayerOrGroup>,
|
|
53
|
+
]
|
|
54
|
+
? ComputeLayers<Tail, ProvideMerge<R, ComputeLayer<Head>>>
|
|
55
|
+
: R;
|
|
56
|
+
|
|
57
|
+
export type ProvideMerge<A extends LayerAny, B extends LayerAny> = Layer.Layer<
|
|
58
|
+
Layer.Success<A | B>,
|
|
59
|
+
Layer.Error<A | B>,
|
|
60
|
+
Exclude<Layer.Services<A>, Layer.Success<B>> | Layer.Services<B>
|
|
61
|
+
>;
|
|
62
|
+
|
|
63
|
+
export type Provide<A extends LayerAny, B extends LayerAny> = Layer.Layer<
|
|
64
|
+
Layer.Success<A | B>,
|
|
65
|
+
Layer.Error<A | B>,
|
|
66
|
+
Exclude<Layer.Services<A>, Layer.Success<B>> | Layer.Services<B>
|
|
67
|
+
>;
|
|
68
|
+
|
|
69
|
+
type ComputeLayer<L extends LayerOrGroup> =
|
|
70
|
+
L extends Layer.Layer<infer A, infer E, infer R>
|
|
71
|
+
? Layer.Layer<A, E, R>
|
|
72
|
+
: L extends ReadonlyArray<Layer.Layer<infer A, infer E, infer R>>
|
|
73
|
+
? Layer.Layer<A, E, R>
|
|
74
|
+
: never;
|
|
75
|
+
|
|
76
|
+
/** Normalizes LayerOrGroup to a single Layer at runtime. Arrays use Layer.mergeAll. */
|
|
77
|
+
export function normalizeLayerInput<L extends LayerOrGroup>(input: L): LayerAny {
|
|
78
|
+
return toLayer(input);
|
|
79
|
+
}
|