@loutrejs/loutre 0.2.0 → 0.4.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/dist/adapters/bun.d.ts +11 -8
- package/dist/adapters/bun.js +72 -50
- package/dist/adapters/deno.d.ts +9 -7
- package/dist/adapters/deno.js +75 -53
- package/dist/application/binding.js +3 -0
- package/dist/application/index.d.ts +2 -1
- package/dist/application/openapi.d.ts +6 -0
- package/dist/application/openapi.js +14 -19
- package/dist/core/contract.d.ts +47 -7
- package/dist/core/contract.js +59 -10
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/graph/compiler.d.ts +4 -0
- package/dist/graph/compiler.js +228 -0
- package/dist/graph/graph.js +56 -31
- package/dist/graph/index.d.ts +2 -1
- package/dist/graph/index.js +2 -1
- package/dist/graph/ir.d.ts +24 -8
- package/dist/http/client.d.ts +72 -0
- package/dist/http/client.js +258 -0
- package/dist/http/definitions.d.ts +13 -5
- package/dist/http/definitions.js +6 -2
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.js +1 -0
- package/dist/message-port/index.d.ts +12 -4
- package/dist/message-port/index.js +9 -2
- package/dist/runtime/application.d.ts +2 -1
- package/dist/runtime/application.js +18 -3
- package/dist/runtime/di.d.ts +1 -0
- package/dist/runtime/di.js +31 -0
- package/package.json +1 -1
package/dist/adapters/bun.d.ts
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
import { type ApplicationDefinition, type BootstrapArguments, type HasHttp, type HostBindingApplication } from '../application/index.js';
|
|
2
2
|
type IsAny<TValue> = 0 extends 1 & TValue ? true : false;
|
|
3
3
|
type HttpApplication<TDefinition extends ApplicationDefinition> = IsAny<TDefinition> extends true ? TDefinition : HasHttp<TDefinition> extends true ? TDefinition : never;
|
|
4
|
-
export type
|
|
4
|
+
export type BunCreateOptions<TDefinition extends ApplicationDefinition> = {
|
|
5
5
|
readonly application: HttpApplication<TDefinition>;
|
|
6
|
+
readonly environment?: unknown;
|
|
7
|
+
} & BootstrapArguments<TDefinition>;
|
|
8
|
+
export interface BunServeOptions {
|
|
6
9
|
readonly port?: number;
|
|
7
10
|
readonly hostname?: string;
|
|
8
11
|
readonly shutdownHooks?: boolean;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export interface BunServeHandle<TDefinition extends ApplicationDefinition = ApplicationDefinition> {
|
|
12
|
-
readonly application: HostBindingApplication<TDefinition>;
|
|
12
|
+
}
|
|
13
|
+
export interface BunListenerHandle {
|
|
13
14
|
readonly port: number;
|
|
14
|
-
close(signal?: string): Promise<void>;
|
|
15
15
|
}
|
|
16
|
+
export type BunRuntimeApplication<TDefinition extends ApplicationDefinition = ApplicationDefinition> = HostBindingApplication<TDefinition> & {
|
|
17
|
+
serve(options?: BunServeOptions): Promise<BunListenerHandle>;
|
|
18
|
+
};
|
|
16
19
|
export declare const bunRuntime: {
|
|
17
20
|
readonly runtime: 'bun';
|
|
18
21
|
readonly capabilities: Set<string>;
|
|
19
|
-
readonly
|
|
22
|
+
readonly create: typeof create;
|
|
20
23
|
};
|
|
21
|
-
declare function
|
|
24
|
+
declare function create<const TDefinition extends ApplicationDefinition>(options: BunCreateOptions<TDefinition>): Promise<BunRuntimeApplication<TDefinition>>;
|
|
22
25
|
export {};
|
package/dist/adapters/bun.js
CHANGED
|
@@ -15,16 +15,13 @@ export const bunRuntime = {
|
|
|
15
15
|
'env.runtime',
|
|
16
16
|
'crypto.random',
|
|
17
17
|
]),
|
|
18
|
-
|
|
18
|
+
create,
|
|
19
19
|
};
|
|
20
|
-
async function
|
|
20
|
+
async function create(options) {
|
|
21
21
|
assertRuntimeEngine('bun');
|
|
22
22
|
const bun = bunGlobal();
|
|
23
|
-
if (!bun?.serve) {
|
|
24
|
-
throw new Error('LUTRE_BUN_UNAVAILABLE: Bun.serve() is not available.');
|
|
25
|
-
}
|
|
26
23
|
const startedAt = performance.now();
|
|
27
|
-
const environment = bun
|
|
24
|
+
const environment = bun?.env ?? {};
|
|
28
25
|
const presentation = startStartupPresentation({ version: LOUTRE_VERSION }, {
|
|
29
26
|
terminal: detectPresentationTerminal(process.stdout, environment),
|
|
30
27
|
write: (value) => console.log(value),
|
|
@@ -37,53 +34,32 @@ async function serve(options) {
|
|
|
37
34
|
const http = 'http' in host ? host.http : undefined;
|
|
38
35
|
if (!http) {
|
|
39
36
|
await host.application.close();
|
|
40
|
-
throw new Error('LUTRE_RUNTIME_HTTP_REQUIRED: bunRuntime.
|
|
37
|
+
throw new Error('LUTRE_RUNTIME_HTTP_REQUIRED: bunRuntime.create() requires an HTTP-capable Application.');
|
|
41
38
|
}
|
|
42
39
|
await host.application.init();
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
const application = host.application;
|
|
41
|
+
const closeApplication = host.application.close.bind(host.application);
|
|
45
42
|
let server;
|
|
46
|
-
|
|
47
|
-
let
|
|
48
|
-
while (true) {
|
|
49
|
-
try {
|
|
50
|
-
server = bun.serve({
|
|
51
|
-
port,
|
|
52
|
-
...(options.hostname === undefined
|
|
53
|
-
? {}
|
|
54
|
-
: { hostname: options.hostname }),
|
|
55
|
-
fetch: createBunFetchDriver(http),
|
|
56
|
-
});
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
catch (error) {
|
|
60
|
-
if (requestedPort !== undefined || !canRetryOnNextPort(error, port)) {
|
|
61
|
-
await host.application.close().catch(() => undefined);
|
|
62
|
-
throw error;
|
|
63
|
-
}
|
|
64
|
-
port += 1;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
presentation.ready({
|
|
68
|
-
server: serverUrl(options.hostname, port),
|
|
69
|
-
runtime: `Bun ${bun.version ?? 'unknown'}`,
|
|
70
|
-
environment: environment.NODE_ENV ?? 'development',
|
|
71
|
-
startupDurationMs: performance.now() - startedAt,
|
|
72
|
-
});
|
|
43
|
+
let removeShutdownHooks;
|
|
44
|
+
let serving = false;
|
|
73
45
|
let closed = false;
|
|
74
|
-
const
|
|
46
|
+
const close = async (signal) => {
|
|
75
47
|
if (closed)
|
|
76
48
|
return;
|
|
77
49
|
closed = true;
|
|
50
|
+
removeShutdownHooks?.();
|
|
51
|
+
removeShutdownHooks = undefined;
|
|
78
52
|
const errors = [];
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
53
|
+
if (server) {
|
|
54
|
+
try {
|
|
55
|
+
await server.stop(true);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
errors.push(error);
|
|
59
|
+
}
|
|
84
60
|
}
|
|
85
61
|
try {
|
|
86
|
-
await
|
|
62
|
+
await closeApplication(signal);
|
|
87
63
|
}
|
|
88
64
|
catch (error) {
|
|
89
65
|
errors.push(error);
|
|
@@ -91,14 +67,60 @@ async function serve(options) {
|
|
|
91
67
|
if (errors.length > 0)
|
|
92
68
|
throw new AggregateError(errors, 'Bun runtime shutdown failed');
|
|
93
69
|
};
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
70
|
+
const serve = async (serveOptions = {}) => {
|
|
71
|
+
if (closed) {
|
|
72
|
+
throw new Error('LUTRE_APP_STOPPED: Application is stopped.');
|
|
73
|
+
}
|
|
74
|
+
if (serving) {
|
|
75
|
+
throw new Error('LUTRE_RUNTIME_ALREADY_SERVING: Bun runtime Application is already serving.');
|
|
76
|
+
}
|
|
77
|
+
if (!bun?.serve) {
|
|
78
|
+
await close().catch(() => undefined);
|
|
79
|
+
throw new Error('LUTRE_BUN_UNAVAILABLE: Bun.serve() is not available.');
|
|
80
|
+
}
|
|
81
|
+
serving = true;
|
|
82
|
+
try {
|
|
83
|
+
if ('triggers' in application)
|
|
84
|
+
await application.triggers.start();
|
|
85
|
+
const requestedPort = serveOptions.port;
|
|
86
|
+
let port = requestedPort ?? 3000;
|
|
87
|
+
while (true) {
|
|
88
|
+
try {
|
|
89
|
+
server = bun.serve({
|
|
90
|
+
port,
|
|
91
|
+
...(serveOptions.hostname === undefined
|
|
92
|
+
? {}
|
|
93
|
+
: { hostname: serveOptions.hostname }),
|
|
94
|
+
fetch: createBunFetchDriver(http),
|
|
95
|
+
});
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (requestedPort !== undefined || !canRetryOnNextPort(error, port)) {
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
port += 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
presentation.ready({
|
|
106
|
+
server: serverUrl(serveOptions.hostname, port),
|
|
107
|
+
runtime: `Bun ${bun.version ?? 'unknown'}`,
|
|
108
|
+
environment: environment.NODE_ENV ?? 'development',
|
|
109
|
+
startupDurationMs: performance.now() - startedAt,
|
|
110
|
+
});
|
|
111
|
+
removeShutdownHooks =
|
|
112
|
+
serveOptions.shutdownHooks === false
|
|
113
|
+
? undefined
|
|
114
|
+
: registerBunShutdownHooks(close);
|
|
115
|
+
return { port };
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
await close().catch(() => undefined);
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
100
121
|
};
|
|
101
|
-
|
|
122
|
+
Object.assign(application, { serve, close });
|
|
123
|
+
return application;
|
|
102
124
|
}
|
|
103
125
|
function registerBunShutdownHooks(close) {
|
|
104
126
|
const handlers = new Map();
|
package/dist/adapters/deno.d.ts
CHANGED
|
@@ -5,26 +5,28 @@ export type DenoRuntimeOptions<TDefinition extends ApplicationDefinition> = {
|
|
|
5
5
|
readonly application: HttpApplication<TDefinition>;
|
|
6
6
|
readonly environment?: unknown;
|
|
7
7
|
} & BootstrapArguments<TDefinition>;
|
|
8
|
-
export type
|
|
8
|
+
export type DenoCreateOptions<TDefinition extends ApplicationDefinition> = DenoRuntimeOptions<TDefinition>;
|
|
9
|
+
export interface DenoServeOptions {
|
|
9
10
|
readonly port?: number;
|
|
10
11
|
readonly hostname?: string;
|
|
11
12
|
readonly shutdownHooks?: boolean;
|
|
12
|
-
}
|
|
13
|
+
}
|
|
13
14
|
export interface DenoBinding {
|
|
14
15
|
fetch(request: Request): Promise<Response>;
|
|
15
16
|
close(signal?: string): Promise<void>;
|
|
16
17
|
}
|
|
17
|
-
export interface
|
|
18
|
-
readonly application: HostBindingApplication<TDefinition>;
|
|
18
|
+
export interface DenoListenerHandle {
|
|
19
19
|
readonly port: number;
|
|
20
|
-
close(signal?: string): Promise<void>;
|
|
21
20
|
}
|
|
21
|
+
export type DenoRuntimeApplication<TDefinition extends ApplicationDefinition = ApplicationDefinition> = HostBindingApplication<TDefinition> & {
|
|
22
|
+
serve(options?: DenoServeOptions): Promise<DenoListenerHandle>;
|
|
23
|
+
};
|
|
22
24
|
export declare const denoRuntime: {
|
|
23
25
|
readonly runtime: 'deno';
|
|
24
26
|
readonly capabilities: Set<string>;
|
|
25
27
|
readonly bind: typeof bind;
|
|
26
|
-
readonly
|
|
28
|
+
readonly create: typeof create;
|
|
27
29
|
};
|
|
28
30
|
declare function bind<const TDefinition extends ApplicationDefinition>(options: DenoRuntimeOptions<TDefinition>): DenoBinding;
|
|
29
|
-
declare function
|
|
31
|
+
declare function create<const TDefinition extends ApplicationDefinition>(options: DenoCreateOptions<TDefinition>): Promise<DenoRuntimeApplication<TDefinition>>;
|
|
30
32
|
export {};
|
package/dist/adapters/deno.js
CHANGED
|
@@ -16,7 +16,7 @@ export const denoRuntime = {
|
|
|
16
16
|
'crypto.random',
|
|
17
17
|
]),
|
|
18
18
|
bind,
|
|
19
|
-
|
|
19
|
+
create,
|
|
20
20
|
};
|
|
21
21
|
function bind(options) {
|
|
22
22
|
assertRuntimeEngine('deno');
|
|
@@ -34,18 +34,15 @@ function bind(options) {
|
|
|
34
34
|
close: (signal) => invocation.application.close(signal),
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
async function
|
|
37
|
+
async function create(options) {
|
|
38
38
|
assertRuntimeEngine('deno');
|
|
39
39
|
const deno = denoGlobal();
|
|
40
|
-
if (!deno?.serve) {
|
|
41
|
-
throw new Error('LUTRE_DENO_UNAVAILABLE: Deno.serve() is not available.');
|
|
42
|
-
}
|
|
43
40
|
const startedAt = performance.now();
|
|
44
|
-
const isTTY = deno
|
|
41
|
+
const isTTY = deno?.stdout?.isTerminal?.() === true;
|
|
45
42
|
const presentation = startStartupPresentation({ version: LOUTRE_VERSION }, {
|
|
46
43
|
terminal: {
|
|
47
44
|
isTTY,
|
|
48
|
-
color: isTTY && deno
|
|
45
|
+
color: isTTY && deno?.env?.get?.('NO_COLOR') === undefined,
|
|
49
46
|
},
|
|
50
47
|
write: (value) => console.log(value),
|
|
51
48
|
});
|
|
@@ -53,55 +50,32 @@ async function serve(options) {
|
|
|
53
50
|
const http = 'http' in host ? host.http : undefined;
|
|
54
51
|
if (!http) {
|
|
55
52
|
await host.application.close();
|
|
56
|
-
throw new Error('LUTRE_RUNTIME_HTTP_REQUIRED: denoRuntime.
|
|
53
|
+
throw new Error('LUTRE_RUNTIME_HTTP_REQUIRED: denoRuntime.create() requires an HTTP-capable Application.');
|
|
57
54
|
}
|
|
58
55
|
await host.application.init();
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
const application = host.application;
|
|
57
|
+
const closeApplication = host.application.close.bind(host.application);
|
|
61
58
|
let server;
|
|
62
|
-
|
|
63
|
-
let
|
|
64
|
-
while (true) {
|
|
65
|
-
try {
|
|
66
|
-
server = deno.serve({
|
|
67
|
-
port,
|
|
68
|
-
...(options.hostname === undefined
|
|
69
|
-
? {}
|
|
70
|
-
: { hostname: options.hostname }),
|
|
71
|
-
onListen: () => undefined,
|
|
72
|
-
}, createDenoFetchDriver(http));
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
if (requestedPort !== undefined || !canRetryOnNextPort(error, port)) {
|
|
77
|
-
await host.application.close().catch(() => undefined);
|
|
78
|
-
throw error;
|
|
79
|
-
}
|
|
80
|
-
port += 1;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
presentation.ready({
|
|
84
|
-
server: serverUrl(options.hostname, port),
|
|
85
|
-
runtime: `Deno ${deno.version?.deno ?? 'unknown'}`,
|
|
86
|
-
environment: deno.env?.get?.('DENO_ENV') ??
|
|
87
|
-
deno.env?.get?.('NODE_ENV') ??
|
|
88
|
-
'development',
|
|
89
|
-
startupDurationMs: performance.now() - startedAt,
|
|
90
|
-
});
|
|
59
|
+
let removeShutdownHooks;
|
|
60
|
+
let serving = false;
|
|
91
61
|
let closed = false;
|
|
92
|
-
const
|
|
62
|
+
const close = async (signal) => {
|
|
93
63
|
if (closed)
|
|
94
64
|
return;
|
|
95
65
|
closed = true;
|
|
66
|
+
removeShutdownHooks?.();
|
|
67
|
+
removeShutdownHooks = undefined;
|
|
96
68
|
const errors = [];
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
69
|
+
if (server) {
|
|
70
|
+
try {
|
|
71
|
+
await server.shutdown();
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
errors.push(error);
|
|
75
|
+
}
|
|
102
76
|
}
|
|
103
77
|
try {
|
|
104
|
-
await
|
|
78
|
+
await closeApplication(signal);
|
|
105
79
|
}
|
|
106
80
|
catch (error) {
|
|
107
81
|
errors.push(error);
|
|
@@ -109,14 +83,62 @@ async function serve(options) {
|
|
|
109
83
|
if (errors.length > 0)
|
|
110
84
|
throw new AggregateError(errors, 'Deno runtime shutdown failed');
|
|
111
85
|
};
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
86
|
+
const serve = async (serveOptions = {}) => {
|
|
87
|
+
if (closed) {
|
|
88
|
+
throw new Error('LUTRE_APP_STOPPED: Application is stopped.');
|
|
89
|
+
}
|
|
90
|
+
if (serving) {
|
|
91
|
+
throw new Error('LUTRE_RUNTIME_ALREADY_SERVING: Deno runtime Application is already serving.');
|
|
92
|
+
}
|
|
93
|
+
if (!deno?.serve) {
|
|
94
|
+
await close().catch(() => undefined);
|
|
95
|
+
throw new Error('LUTRE_DENO_UNAVAILABLE: Deno.serve() is not available.');
|
|
96
|
+
}
|
|
97
|
+
serving = true;
|
|
98
|
+
try {
|
|
99
|
+
if ('triggers' in application)
|
|
100
|
+
await application.triggers.start();
|
|
101
|
+
const requestedPort = serveOptions.port;
|
|
102
|
+
let port = requestedPort ?? 3000;
|
|
103
|
+
while (true) {
|
|
104
|
+
try {
|
|
105
|
+
server = deno.serve({
|
|
106
|
+
port,
|
|
107
|
+
...(serveOptions.hostname === undefined
|
|
108
|
+
? {}
|
|
109
|
+
: { hostname: serveOptions.hostname }),
|
|
110
|
+
onListen: () => undefined,
|
|
111
|
+
}, createDenoFetchDriver(http));
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (requestedPort !== undefined || !canRetryOnNextPort(error, port)) {
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
port += 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
presentation.ready({
|
|
122
|
+
server: serverUrl(serveOptions.hostname, port),
|
|
123
|
+
runtime: `Deno ${deno.version?.deno ?? 'unknown'}`,
|
|
124
|
+
environment: deno.env?.get?.('DENO_ENV') ??
|
|
125
|
+
deno.env?.get?.('NODE_ENV') ??
|
|
126
|
+
'development',
|
|
127
|
+
startupDurationMs: performance.now() - startedAt,
|
|
128
|
+
});
|
|
129
|
+
removeShutdownHooks =
|
|
130
|
+
serveOptions.shutdownHooks === false
|
|
131
|
+
? undefined
|
|
132
|
+
: registerDenoShutdownHooks(deno, close);
|
|
133
|
+
return { port };
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
await close().catch(() => undefined);
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
118
139
|
};
|
|
119
|
-
|
|
140
|
+
Object.assign(application, { serve, close });
|
|
141
|
+
return application;
|
|
120
142
|
}
|
|
121
143
|
function registerDenoShutdownHooks(deno, close) {
|
|
122
144
|
if (!deno.addSignalListener || !deno.removeSignalListener) {
|
|
@@ -109,6 +109,9 @@ function createRuntimeState(options) {
|
|
|
109
109
|
function createInvocationApplication(state) {
|
|
110
110
|
const application = {
|
|
111
111
|
graph: state.graph,
|
|
112
|
+
get(token) {
|
|
113
|
+
return state.runtime.get(token);
|
|
114
|
+
},
|
|
112
115
|
async init() {
|
|
113
116
|
await state.runtime.initialize();
|
|
114
117
|
return application;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ArgsClass, ModuleProtocols, ModuleInstance, SchemaInput, TaskArguments, TaskDescriptor, TaskOutput, TriggerDescriptor } from '../core/index.js';
|
|
1
|
+
import type { ArgsClass, ModuleProtocols, ModuleInstance, SchemaInput, TaskArguments, TaskDescriptor, TaskOutput, TokenLike, TokenValue, TriggerDescriptor } from '../core/index.js';
|
|
2
2
|
import type { ApplicationGraphIR } from '../graph/index.js';
|
|
3
3
|
import type { Logger } from '../runtime/index.js';
|
|
4
4
|
export interface ApplicationDefinitionOptions<TModules extends readonly ModuleInstance[], TArguments extends ArgsClass | undefined, TTasks extends readonly TaskDescriptor<any, any>[], TTriggers extends readonly TriggerDescriptor[]> {
|
|
@@ -27,6 +27,7 @@ export type BootstrapArguments<TDefinition extends ApplicationDefinition> = TDef
|
|
|
27
27
|
};
|
|
28
28
|
export interface BaseApplication {
|
|
29
29
|
readonly graph: ApplicationGraphIR;
|
|
30
|
+
get<TToken extends TokenLike>(token: TToken): TokenValue<TToken>;
|
|
30
31
|
init(): Promise<this>;
|
|
31
32
|
close(signal?: string): Promise<void>;
|
|
32
33
|
}
|
|
@@ -8,9 +8,15 @@ export interface OpenApiServer {
|
|
|
8
8
|
readonly url: string;
|
|
9
9
|
readonly description?: string;
|
|
10
10
|
}
|
|
11
|
+
export interface OpenApiOperationIdContext {
|
|
12
|
+
readonly procedure: string;
|
|
13
|
+
readonly method: string;
|
|
14
|
+
readonly path: string;
|
|
15
|
+
}
|
|
11
16
|
export interface GenerateOpenApiOptions {
|
|
12
17
|
readonly info: OpenApiInfo;
|
|
13
18
|
readonly servers?: readonly OpenApiServer[];
|
|
19
|
+
readonly operationId?: (context: OpenApiOperationIdContext) => string | undefined;
|
|
14
20
|
}
|
|
15
21
|
export interface OpenApiDocument {
|
|
16
22
|
readonly openapi: '3.2.0';
|
|
@@ -13,7 +13,7 @@ const FIXED_METHODS = new Set([
|
|
|
13
13
|
'query',
|
|
14
14
|
]);
|
|
15
15
|
export function generateOpenApi(application, options) {
|
|
16
|
-
|
|
16
|
+
assertValidCompilation(compileApplication({
|
|
17
17
|
modules: application.modules,
|
|
18
18
|
...(application.arguments === undefined
|
|
19
19
|
? {}
|
|
@@ -21,11 +21,6 @@ export function generateOpenApi(application, options) {
|
|
|
21
21
|
tasks: application.tasks,
|
|
22
22
|
triggers: application.triggers,
|
|
23
23
|
}));
|
|
24
|
-
const executable = new Set(graph.executions
|
|
25
|
-
.filter((execution) => execution.kind === 'protocol' && execution.protocol === 'http')
|
|
26
|
-
.map((execution) => execution.kind === 'protocol'
|
|
27
|
-
? `${execution.implementation}\u0000${execution.procedure}`
|
|
28
|
-
: ''));
|
|
29
24
|
const registry = new SchemaRegistry();
|
|
30
25
|
const paths = {};
|
|
31
26
|
const operationIds = new Set();
|
|
@@ -34,20 +29,15 @@ export function generateOpenApi(application, options) {
|
|
|
34
29
|
if (implementation.protocol !== 'http')
|
|
35
30
|
continue;
|
|
36
31
|
for (const procedure of implementation.procedures) {
|
|
37
|
-
if (!executable.has(`${implementation.name}\u0000${procedure}`))
|
|
38
|
-
continue;
|
|
39
32
|
const protocol = implementation.contract.procedures[procedure]?.protocols.http;
|
|
40
33
|
if (!protocol || protocol.protocol !== 'http')
|
|
41
34
|
continue;
|
|
42
35
|
const typed = protocol;
|
|
43
36
|
const target = {
|
|
44
37
|
definition: typed.definition,
|
|
45
|
-
...(implementation.contract.name === undefined
|
|
46
|
-
? {}
|
|
47
|
-
: { contractName: implementation.contract.name }),
|
|
48
38
|
procedure,
|
|
49
39
|
};
|
|
50
|
-
const operation = createOperation(target, registry, operationIds);
|
|
40
|
+
const operation = createOperation(target, registry, operationIds, options.operationId);
|
|
51
41
|
attachOperation(paths, typed.definition, operation);
|
|
52
42
|
}
|
|
53
43
|
}
|
|
@@ -62,12 +52,17 @@ export function generateOpenApi(application, options) {
|
|
|
62
52
|
...(Object.keys(schemas).length === 0 ? {} : { components: { schemas } }),
|
|
63
53
|
};
|
|
64
54
|
}
|
|
65
|
-
function createOperation(target, registry, operationIds) {
|
|
55
|
+
function createOperation(target, registry, operationIds, resolveOperationId) {
|
|
66
56
|
const definition = target.definition;
|
|
67
|
-
const operationId =
|
|
68
|
-
|
|
69
|
-
:
|
|
70
|
-
|
|
57
|
+
const operationId = resolveOperationId?.({
|
|
58
|
+
procedure: target.procedure,
|
|
59
|
+
method: definition.method,
|
|
60
|
+
path: definition.path,
|
|
61
|
+
});
|
|
62
|
+
if (operationId !== undefined) {
|
|
63
|
+
if (operationId.length === 0) {
|
|
64
|
+
throw openApiError('LUTRE_OPENAPI_OPERATION_ID_002', `operationId must not be empty for ${describeTarget(target)}`);
|
|
65
|
+
}
|
|
71
66
|
if (operationIds.has(operationId)) {
|
|
72
67
|
throw openApiError('LUTRE_OPENAPI_OPERATION_ID_001', `Duplicate operationId: ${operationId}`);
|
|
73
68
|
}
|
|
@@ -322,7 +317,7 @@ function rebaseLocalDefinitions(schema, schemaComponentName) {
|
|
|
322
317
|
return visit(schema);
|
|
323
318
|
}
|
|
324
319
|
function componentName(target, suffix) {
|
|
325
|
-
return sanitizeComponentName(`${target.
|
|
320
|
+
return sanitizeComponentName(`${target.definition.method} ${target.definition.path} ${target.procedure} ${suffix}`);
|
|
326
321
|
}
|
|
327
322
|
function sanitizeComponentName(value) {
|
|
328
323
|
const sanitized = value.replace(/[^A-Za-z0-9_.-]+/g, '_');
|
|
@@ -340,7 +335,7 @@ function escapeJsonPointer(value) {
|
|
|
340
335
|
return value.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
341
336
|
}
|
|
342
337
|
function describeTarget(target) {
|
|
343
|
-
return `${target.
|
|
338
|
+
return `${target.definition.method.toUpperCase()} ${target.definition.path} (${target.procedure})`;
|
|
344
339
|
}
|
|
345
340
|
function dedupeJsonSchemas(schemas) {
|
|
346
341
|
const seen = new Set();
|
package/dist/core/contract.d.ts
CHANGED
|
@@ -12,17 +12,57 @@ export interface ProcedureDefinition<TProtocols extends Record<string, ProtocolD
|
|
|
12
12
|
readonly kind: 'procedure';
|
|
13
13
|
readonly protocols: TProtocols;
|
|
14
14
|
}
|
|
15
|
-
export
|
|
16
|
-
readonly
|
|
17
|
-
|
|
15
|
+
export interface ProtocolGroup<TName extends string = string, TProcedures extends Record<string, ProtocolDescriptor<TName>> = Record<string, ProtocolDescriptor<TName>>> {
|
|
16
|
+
readonly kind: 'protocol-group';
|
|
17
|
+
readonly protocol: TName;
|
|
18
|
+
readonly procedures: TProcedures;
|
|
19
|
+
}
|
|
20
|
+
export declare function protocolGroup<const TName extends string, const TProcedures extends Record<string, ProtocolDescriptor<TName>>>(protocol: TName, procedures: TProcedures): ProtocolGroup<TName, TProcedures>;
|
|
18
21
|
export interface ContractDefinition<TProcedures extends Record<string, ProcedureDefinition> = Record<string, ProcedureDefinition>> {
|
|
19
22
|
readonly kind: 'contract';
|
|
20
|
-
readonly name?: string;
|
|
21
23
|
readonly procedures: TProcedures;
|
|
22
24
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
type ProcedureNamesOfGroup<TGroup> = TGroup extends ProtocolGroup<string, infer TProcedures> ? keyof TProcedures & string : never;
|
|
26
|
+
type ProcedureNamesOfGroups<TGroups extends readonly ProtocolGroup[]> = ProcedureNamesOfGroup<TGroups[number]>;
|
|
27
|
+
type ProtocolOfGroupProcedure<TGroup, TProcedure extends string> = TGroup extends ProtocolGroup<infer TProtocol, infer TProcedures> ? TProcedure extends keyof TProcedures ? {
|
|
28
|
+
readonly [K in TProtocol]: TProcedures[TProcedure];
|
|
29
|
+
} : never : never;
|
|
30
|
+
type ProceduresFromGroups<TGroups extends readonly ProtocolGroup[]> = {
|
|
31
|
+
[TProcedure in ProcedureNamesOfGroups<TGroups>]: ProcedureDefinition<UnionToIntersection<ProtocolOfGroupProcedure<TGroups[number], TProcedure>> extends infer TProtocols extends Record<string, ProtocolDescriptor> ? TProtocols : never>;
|
|
32
|
+
};
|
|
33
|
+
type NonEmptyArrayConstraint<TValues extends readonly unknown[]> = TValues extends readonly [] ? {
|
|
34
|
+
readonly __requiresAtLeastOneEntry__: never;
|
|
35
|
+
} : unknown;
|
|
36
|
+
type ProcedureProtocolKeysOfGroup<TGroup> = TGroup extends ProtocolGroup<infer TProtocol, infer TProcedures> ? `${keyof TProcedures & string}.${TProtocol}` : never;
|
|
37
|
+
type DuplicateGroupProcedureProtocolKeys<TGroups extends readonly ProtocolGroup[], TSeen extends string = never> = number extends TGroups['length'] ? never : TGroups extends readonly [
|
|
38
|
+
infer THead extends ProtocolGroup,
|
|
39
|
+
...infer TTail extends readonly ProtocolGroup[]
|
|
40
|
+
] ? Extract<ProcedureProtocolKeysOfGroup<THead>, TSeen> | DuplicateGroupProcedureProtocolKeys<TTail, TSeen | ProcedureProtocolKeysOfGroup<THead>> : never;
|
|
41
|
+
type GroupProcedureProtocolUniquenessConstraint<TGroups extends readonly ProtocolGroup[]> = [DuplicateGroupProcedureProtocolKeys<TGroups>] extends [never] ? unknown : {
|
|
42
|
+
readonly __duplicateProcedureProtocol__: DuplicateGroupProcedureProtocolKeys<TGroups>;
|
|
43
|
+
};
|
|
44
|
+
declare function defineContract<const TGroups extends readonly ProtocolGroup[]>(groups: TGroups & NonEmptyArrayConstraint<TGroups> & GroupProcedureProtocolUniquenessConstraint<TGroups> & DispatchKeyUniquenessConstraint<ProceduresFromGroups<TGroups>>): ContractDefinition<ProceduresFromGroups<TGroups>>;
|
|
45
|
+
type ProcedureNamesOfContract<TContract> = TContract extends ContractDefinition<infer TProcedures> ? keyof TProcedures & string : never;
|
|
46
|
+
type ProcedureNamesOfContracts<TContracts extends readonly ContractDefinition[]> = ProcedureNamesOfContract<TContracts[number]>;
|
|
47
|
+
type ProtocolsOfContractProcedure<TContract, TProcedure extends string> = TContract extends ContractDefinition<infer TProcedures> ? TProcedure extends keyof TProcedures ? TProcedures[TProcedure]['protocols'] : never : never;
|
|
48
|
+
type UnionToIntersection<T> = (T extends unknown ? (value: T) => void : never) extends (value: infer TIntersection) => void ? TIntersection : never;
|
|
49
|
+
type MergedProcedures<TContracts extends readonly ContractDefinition[]> = {
|
|
50
|
+
[TProcedure in ProcedureNamesOfContracts<TContracts>]: ProcedureDefinition<UnionToIntersection<ProtocolsOfContractProcedure<TContracts[number], TProcedure>> extends infer TProtocols extends Record<string, ProtocolDescriptor> ? TProtocols : never>;
|
|
51
|
+
};
|
|
52
|
+
type ProcedureProtocolKeysOfContract<TContract> = TContract extends ContractDefinition<infer TProcedures> ? {
|
|
53
|
+
[TProcedure in keyof TProcedures & string]: `${TProcedure}.${keyof TProcedures[TProcedure]['protocols'] & string}`;
|
|
54
|
+
}[keyof TProcedures & string] : never;
|
|
55
|
+
type DuplicateContractProcedureProtocolKeys<TContracts extends readonly ContractDefinition[], TSeen extends string = never> = number extends TContracts['length'] ? never : TContracts extends readonly [
|
|
56
|
+
infer THead extends ContractDefinition,
|
|
57
|
+
...infer TTail extends readonly ContractDefinition[]
|
|
58
|
+
] ? Extract<ProcedureProtocolKeysOfContract<THead>, TSeen> | DuplicateContractProcedureProtocolKeys<TTail, TSeen | ProcedureProtocolKeysOfContract<THead>> : never;
|
|
59
|
+
type ContractProcedureProtocolUniquenessConstraint<TContracts extends readonly ContractDefinition[]> = [DuplicateContractProcedureProtocolKeys<TContracts>] extends [never] ? unknown : {
|
|
60
|
+
readonly __duplicateProcedureProtocol__: DuplicateContractProcedureProtocolKeys<TContracts>;
|
|
61
|
+
};
|
|
62
|
+
declare function mergeContracts<const TContracts extends readonly ContractDefinition[]>(contracts: TContracts & NonEmptyArrayConstraint<TContracts> & ContractProcedureProtocolUniquenessConstraint<TContracts> & DispatchKeyUniquenessConstraint<MergedProcedures<TContracts>>): ContractDefinition<MergedProcedures<TContracts>>;
|
|
63
|
+
export declare const contract: typeof defineContract & {
|
|
64
|
+
merge: typeof mergeContracts;
|
|
65
|
+
};
|
|
26
66
|
type ProtocolDispatchEntries<TProcedures extends Record<string, ProcedureDefinition>> = {
|
|
27
67
|
[TProcedure in keyof TProcedures & string]: {
|
|
28
68
|
[TProtocol in keyof TProcedures[TProcedure]['protocols'] & string]: TProcedures[TProcedure]['protocols'][TProtocol] extends {
|