@metacall/protocol 0.1.25 → 0.1.27
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 +34 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/language.d.ts +11 -1
- package/dist/language.js +55 -13
- package/dist/package.d.ts +5 -2
- package/dist/package.js +6 -17
- package/dist/protocol.d.ts +83 -0
- package/dist/protocol.js +154 -56
- package/dist/token.js +1 -1
- package/package.json +5 -5
- package/src/index.ts +1 -0
- package/src/language.ts +69 -13
- package/src/package.ts +10 -19
- package/src/protocol.ts +241 -59
- package/tsconfig.json +3 -2
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# MetaCall Protocol
|
|
2
|
+
|
|
3
|
+
This repository contains the protocol implementation for MetaCall FaaS. It is a client that allows deploying to [MetaCall FaaS](https://dashboard.metacall.io) or its [open source reimplementation](https://github.com/metacall/faas) written in TypeScript.
|
|
4
|
+
|
|
5
|
+
## Documentation
|
|
6
|
+
|
|
7
|
+
Explore the detailed documentation for the MetaCall Protocol in the [doc](doc) directory. It provides comprehensive information on the protocol, its implementation details, and best practices for integration.
|
|
8
|
+
|
|
9
|
+
## [Core](https://github.com/metacall/core)
|
|
10
|
+
|
|
11
|
+
The ultimate polyglot programming experience.
|
|
12
|
+
|
|
13
|
+
## [Deploy](https://github.com/metacall/deploy)
|
|
14
|
+
|
|
15
|
+
Tool for deploying into MetaCall FaaS platform.
|
|
16
|
+
|
|
17
|
+
## [Examples](https://github.com/metacall/examples)
|
|
18
|
+
|
|
19
|
+
A collection of use cases and examples to be deployed in MetaCall.
|
|
20
|
+
|
|
21
|
+
## [FaaS](https://github.com/metacall/faas)
|
|
22
|
+
|
|
23
|
+
The FaaS platform reimplementation for MetaCall.
|
|
24
|
+
|
|
25
|
+
## Testing
|
|
26
|
+
|
|
27
|
+
The [test](test) directory contains various tests for the MetaCall Protocol. These tests are designed to ensure the reliability and correctness of the protocol implementation. If you encounter any issues or inconsistencies, please report them by opening an [issue](https://github.com/metacall/protocol-repo/issues).
|
|
28
|
+
|
|
29
|
+
## Contributing
|
|
30
|
+
|
|
31
|
+
We welcome contributions from the community. If you have ideas, suggestions, or want to report a bug, please feel free to open an [issue](https://github.com/metacall/protocol/issues) or submit a [pull request](https://github.com/metacall/protocol/pulls).
|
|
32
|
+
## License
|
|
33
|
+
|
|
34
|
+
This repository is licensed under the [Apache License, Version 2.0](LICENSE). Feel free to use, modify, and distribute the code in accordance with the terms specified in the license.
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -21,4 +21,5 @@ __exportStar(require("./plan"), exports);
|
|
|
21
21
|
__exportStar(require("./token"), exports);
|
|
22
22
|
const protocol_1 = __importDefault(require("./protocol"));
|
|
23
23
|
__exportStar(require("./protocol"), exports);
|
|
24
|
+
__exportStar(require("./signup"), exports);
|
|
24
25
|
exports.default = protocol_1.default;
|
package/dist/language.d.ts
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import { LanguageId } from './deployment';
|
|
2
|
+
export declare type Runner = 'nodejs' | 'python' | 'ruby' | 'csharp';
|
|
3
|
+
export interface RunnerInfo {
|
|
4
|
+
id: Runner;
|
|
5
|
+
languageId: LanguageId;
|
|
6
|
+
filePatterns: RegExp[];
|
|
7
|
+
installCommand: string;
|
|
8
|
+
displayName: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const Runners: Record<Runner, RunnerInfo>;
|
|
2
11
|
interface Language {
|
|
3
12
|
tag: string;
|
|
4
13
|
displayName: string;
|
|
5
14
|
hexColor: string;
|
|
6
15
|
fileExtRegex: RegExp;
|
|
7
|
-
runnerName?:
|
|
16
|
+
runnerName?: Runner;
|
|
8
17
|
runnerFilesRegexes: RegExp[];
|
|
9
18
|
}
|
|
10
19
|
export declare const Languages: Record<LanguageId, Language>;
|
|
11
20
|
export declare const DisplayNameToLanguageId: Record<string, LanguageId>;
|
|
12
21
|
export declare const RunnerToDisplayName: (runner: string) => string;
|
|
22
|
+
export declare const detectRunnersFromFiles: (files: string[]) => Runner[];
|
|
13
23
|
export {};
|
package/dist/language.js
CHANGED
|
@@ -6,7 +6,38 @@
|
|
|
6
6
|
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.RunnerToDisplayName = exports.DisplayNameToLanguageId = exports.Languages = void 0;
|
|
9
|
+
exports.detectRunnersFromFiles = exports.RunnerToDisplayName = exports.DisplayNameToLanguageId = exports.Languages = exports.Runners = void 0;
|
|
10
|
+
const path_1 = require("path");
|
|
11
|
+
exports.Runners = {
|
|
12
|
+
nodejs: {
|
|
13
|
+
id: 'nodejs',
|
|
14
|
+
languageId: 'node',
|
|
15
|
+
filePatterns: [/^package\.json$/],
|
|
16
|
+
installCommand: 'npm install',
|
|
17
|
+
displayName: 'NPM'
|
|
18
|
+
},
|
|
19
|
+
python: {
|
|
20
|
+
id: 'python',
|
|
21
|
+
languageId: 'py',
|
|
22
|
+
filePatterns: [/^requirements\.txt$/],
|
|
23
|
+
installCommand: 'pip install -r requirements.txt',
|
|
24
|
+
displayName: 'Pip'
|
|
25
|
+
},
|
|
26
|
+
ruby: {
|
|
27
|
+
id: 'ruby',
|
|
28
|
+
languageId: 'rb',
|
|
29
|
+
filePatterns: [/^Gemfile$/],
|
|
30
|
+
installCommand: 'bundle install',
|
|
31
|
+
displayName: 'Gem'
|
|
32
|
+
},
|
|
33
|
+
csharp: {
|
|
34
|
+
id: 'csharp',
|
|
35
|
+
languageId: 'cs',
|
|
36
|
+
filePatterns: [/^project\.json$/, /\.csproj$/],
|
|
37
|
+
installCommand: 'dotnet restore',
|
|
38
|
+
displayName: 'NuGet'
|
|
39
|
+
}
|
|
40
|
+
};
|
|
10
41
|
exports.Languages = {
|
|
11
42
|
cs: {
|
|
12
43
|
tag: 'cs',
|
|
@@ -14,7 +45,7 @@ exports.Languages = {
|
|
|
14
45
|
hexColor: '#953dac',
|
|
15
46
|
fileExtRegex: /^cs$/,
|
|
16
47
|
runnerName: 'csharp',
|
|
17
|
-
runnerFilesRegexes:
|
|
48
|
+
runnerFilesRegexes: exports.Runners.csharp.filePatterns
|
|
18
49
|
},
|
|
19
50
|
py: {
|
|
20
51
|
tag: 'py',
|
|
@@ -22,7 +53,7 @@ exports.Languages = {
|
|
|
22
53
|
hexColor: '#ffd43b',
|
|
23
54
|
fileExtRegex: /^py$/,
|
|
24
55
|
runnerName: 'python',
|
|
25
|
-
runnerFilesRegexes:
|
|
56
|
+
runnerFilesRegexes: exports.Runners.python.filePatterns
|
|
26
57
|
},
|
|
27
58
|
rb: {
|
|
28
59
|
tag: 'rb',
|
|
@@ -30,7 +61,7 @@ exports.Languages = {
|
|
|
30
61
|
hexColor: '#e53935',
|
|
31
62
|
fileExtRegex: /^rb$/,
|
|
32
63
|
runnerName: 'ruby',
|
|
33
|
-
runnerFilesRegexes:
|
|
64
|
+
runnerFilesRegexes: exports.Runners.ruby.filePatterns
|
|
34
65
|
},
|
|
35
66
|
node: {
|
|
36
67
|
tag: 'node',
|
|
@@ -38,7 +69,7 @@ exports.Languages = {
|
|
|
38
69
|
hexColor: '#3c873a',
|
|
39
70
|
fileExtRegex: /^js$/,
|
|
40
71
|
runnerName: 'nodejs',
|
|
41
|
-
runnerFilesRegexes:
|
|
72
|
+
runnerFilesRegexes: exports.Runners.nodejs.filePatterns
|
|
42
73
|
},
|
|
43
74
|
ts: {
|
|
44
75
|
tag: 'ts',
|
|
@@ -46,7 +77,7 @@ exports.Languages = {
|
|
|
46
77
|
hexColor: '#007acc',
|
|
47
78
|
fileExtRegex: /^(ts|tsx)$/,
|
|
48
79
|
runnerName: 'nodejs',
|
|
49
|
-
runnerFilesRegexes:
|
|
80
|
+
runnerFilesRegexes: exports.Runners.nodejs.filePatterns
|
|
50
81
|
},
|
|
51
82
|
file: {
|
|
52
83
|
tag: 'file',
|
|
@@ -77,12 +108,23 @@ exports.DisplayNameToLanguageId = Object.keys(exports.Languages).reduce((obj, la
|
|
|
77
108
|
[exports.Languages[lang].displayName]: lang
|
|
78
109
|
}), {});
|
|
79
110
|
const RunnerToDisplayName = (runner) => {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
python: 'Pip',
|
|
83
|
-
ruby: 'Gem',
|
|
84
|
-
csharp: 'NuGet'
|
|
85
|
-
};
|
|
86
|
-
return displayNameMap[runner] || 'Build';
|
|
111
|
+
const match = exports.Runners[runner];
|
|
112
|
+
return match ? match.displayName : 'Build';
|
|
87
113
|
};
|
|
88
114
|
exports.RunnerToDisplayName = RunnerToDisplayName;
|
|
115
|
+
const detectRunnersFromFiles = (files) => {
|
|
116
|
+
const runners = new Set();
|
|
117
|
+
for (const file of files) {
|
|
118
|
+
const fileName = path_1.basename(file);
|
|
119
|
+
for (const runner of Object.values(exports.Runners)) {
|
|
120
|
+
for (const pattern of runner.filePatterns) {
|
|
121
|
+
if (pattern.exec(fileName)) {
|
|
122
|
+
runners.add(runner.id);
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return Array.from(runners);
|
|
129
|
+
};
|
|
130
|
+
exports.detectRunnersFromFiles = detectRunnersFromFiles;
|
package/dist/package.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { MetaCallJSON } from './deployment';
|
|
2
|
+
import { Runner } from './language';
|
|
2
3
|
export declare const findFilesPath: (path?: string, ignoreFiles?: string[]) => Promise<string[]>;
|
|
4
|
+
export declare const pathIsMetaCallJson: (path: string) => boolean;
|
|
3
5
|
export declare const findMetaCallJsons: (files: string[]) => string[];
|
|
4
|
-
export declare const findRunners: (files: string[]) => Set<
|
|
6
|
+
export declare const findRunners: (files: string[]) => Set<Runner>;
|
|
7
|
+
export declare const detectRunners: (path?: string, ignoreFiles?: string[]) => Promise<Runner[]>;
|
|
5
8
|
export declare enum PackageError {
|
|
6
9
|
Empty = "No files found in the current folder",
|
|
7
10
|
JsonNotFound = "No metacall.json found in the current folder",
|
|
@@ -11,7 +14,7 @@ interface PackageDescriptor {
|
|
|
11
14
|
error: PackageError;
|
|
12
15
|
files: string[];
|
|
13
16
|
jsons: string[];
|
|
14
|
-
runners:
|
|
17
|
+
runners: Runner[];
|
|
15
18
|
}
|
|
16
19
|
export declare const generatePackage: (path?: string) => Promise<PackageDescriptor>;
|
|
17
20
|
export declare const generateJsonsFromFiles: (files: string[]) => MetaCallJSON[];
|
package/dist/package.js
CHANGED
|
@@ -16,7 +16,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
16
16
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
17
17
|
};
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
-
exports.generateJsonsFromFiles = exports.generatePackage = exports.PackageError = exports.findRunners = exports.findMetaCallJsons = exports.findFilesPath = void 0;
|
|
19
|
+
exports.generateJsonsFromFiles = exports.generatePackage = exports.PackageError = exports.detectRunners = exports.findRunners = exports.findMetaCallJsons = exports.pathIsMetaCallJson = exports.findFilesPath = void 0;
|
|
20
20
|
const ignore_walk_1 = __importDefault(require("ignore-walk"));
|
|
21
21
|
const path_1 = require("path");
|
|
22
22
|
const language_1 = require("./language");
|
|
@@ -28,24 +28,13 @@ const findFilesPath = async (path = process.cwd(), ignoreFiles = ['.gitignore'])
|
|
|
28
28
|
})).filter(x => !x.startsWith('.git'));
|
|
29
29
|
exports.findFilesPath = findFilesPath;
|
|
30
30
|
const pathIsMetaCallJson = (path) => !!/^metacall(-.+)?\.json$/.exec(path_1.basename(path));
|
|
31
|
-
|
|
31
|
+
exports.pathIsMetaCallJson = pathIsMetaCallJson;
|
|
32
|
+
const findMetaCallJsons = (files) => files.filter(exports.pathIsMetaCallJson);
|
|
32
33
|
exports.findMetaCallJsons = findMetaCallJsons;
|
|
33
|
-
const findRunners = (files) =>
|
|
34
|
-
const runners = new Set();
|
|
35
|
-
for (const file of files) {
|
|
36
|
-
const fileName = path_1.basename(file);
|
|
37
|
-
for (const langId of Object.keys(language_1.Languages)) {
|
|
38
|
-
const lang = language_1.Languages[langId];
|
|
39
|
-
for (const re of lang.runnerFilesRegexes) {
|
|
40
|
-
if (re.exec(fileName) && lang.runnerName) {
|
|
41
|
-
runners.add(lang.runnerName);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return runners;
|
|
47
|
-
};
|
|
34
|
+
const findRunners = (files) => new Set(language_1.detectRunnersFromFiles(files));
|
|
48
35
|
exports.findRunners = findRunners;
|
|
36
|
+
const detectRunners = async (path = process.cwd(), ignoreFiles = ['.gitignore']) => language_1.detectRunnersFromFiles(await exports.findFilesPath(path, ignoreFiles));
|
|
37
|
+
exports.detectRunners = detectRunners;
|
|
49
38
|
var PackageError;
|
|
50
39
|
(function (PackageError) {
|
|
51
40
|
PackageError["Empty"] = "No files found in the current folder";
|
package/dist/protocol.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { AxiosError } from 'axios';
|
|
2
2
|
import { Create, Deployment, LogType, MetaCallJSON } from './deployment';
|
|
3
3
|
import { Plans } from './plan';
|
|
4
|
+
/**
|
|
5
|
+
* Type guard for protocol-specific errors (Axios errors in this case).
|
|
6
|
+
* @param err - The unknown error to check.
|
|
7
|
+
* @returns True if the error is an ProtocolError, false otherwise.
|
|
8
|
+
*/
|
|
4
9
|
export declare const isProtocolError: (err: unknown) => boolean;
|
|
5
10
|
export { AxiosError as ProtocolError };
|
|
6
11
|
declare type SubscriptionMap = Record<string, number>;
|
|
@@ -20,13 +25,47 @@ export interface AddResponse {
|
|
|
20
25
|
export interface Branches {
|
|
21
26
|
branches: [string];
|
|
22
27
|
}
|
|
28
|
+
export declare enum InvokeType {
|
|
29
|
+
Call = "call",
|
|
30
|
+
Await = "await"
|
|
31
|
+
}
|
|
32
|
+
export interface DeployCreateRequest {
|
|
33
|
+
suffix: string;
|
|
34
|
+
resourceType: ResourceType;
|
|
35
|
+
release: string;
|
|
36
|
+
env: {
|
|
37
|
+
name: string;
|
|
38
|
+
value: string;
|
|
39
|
+
}[];
|
|
40
|
+
plan: Plans;
|
|
41
|
+
version: string;
|
|
42
|
+
}
|
|
43
|
+
export interface DeployDeleteRequest {
|
|
44
|
+
prefix: string;
|
|
45
|
+
suffix: string;
|
|
46
|
+
version: string;
|
|
47
|
+
}
|
|
48
|
+
export interface RepositoryAddRequest {
|
|
49
|
+
url: string;
|
|
50
|
+
branch: string;
|
|
51
|
+
jsons: MetaCallJSON[];
|
|
52
|
+
}
|
|
53
|
+
export interface RepositoryBranchListRequest {
|
|
54
|
+
url: string;
|
|
55
|
+
}
|
|
56
|
+
export interface RepositoryFileListRequest {
|
|
57
|
+
url: string;
|
|
58
|
+
branch: string;
|
|
59
|
+
}
|
|
23
60
|
export interface API {
|
|
24
61
|
refresh(): Promise<string>;
|
|
62
|
+
ready(): Promise<boolean>;
|
|
25
63
|
validate(): Promise<boolean>;
|
|
26
64
|
deployEnabled(): Promise<boolean>;
|
|
27
65
|
listSubscriptions(): Promise<SubscriptionMap>;
|
|
28
66
|
listSubscriptionsDeploys(): Promise<SubscriptionDeploy[]>;
|
|
29
67
|
inspect(): Promise<Deployment[]>;
|
|
68
|
+
inspectByName(suffix: string): Promise<Deployment>;
|
|
30
69
|
upload(name: string, blob: unknown, jsons?: MetaCallJSON[], runners?: string[]): Promise<string>;
|
|
31
70
|
add(url: string, branch: string, jsons: MetaCallJSON[]): Promise<AddResponse>;
|
|
32
71
|
deploy(name: string, env: {
|
|
@@ -37,6 +76,50 @@ export interface API {
|
|
|
37
76
|
logs(container: string, type: LogType, suffix: string, prefix: string, version?: string): Promise<string>;
|
|
38
77
|
branchList(url: string): Promise<Branches>;
|
|
39
78
|
fileList(url: string, branch: string): Promise<string[]>;
|
|
79
|
+
invoke<Result, Args = unknown>(type: InvokeType, prefix: string, suffix: string, version: string, name: string, args?: Args): Promise<Result>;
|
|
80
|
+
call<Result, Args = unknown>(prefix: string, suffix: string, version: string, name: string, args?: Args): Promise<Result>;
|
|
81
|
+
await<Result, Args = unknown>(prefix: string, suffix: string, version: string, name: string, args?: Args): Promise<Result>;
|
|
40
82
|
}
|
|
41
83
|
declare const _default: (token: string, baseURL: string) => API;
|
|
42
84
|
export default _default;
|
|
85
|
+
export declare const MaxRetries = 30;
|
|
86
|
+
export declare const MaxRetryInterval = 2000;
|
|
87
|
+
export declare const MaxFuncLength = 64;
|
|
88
|
+
/**
|
|
89
|
+
* Executes an asynchronous function with automatic retry logic.
|
|
90
|
+
*
|
|
91
|
+
* The function will be retried up to `maxRetries` times, waiting `interval`
|
|
92
|
+
* milliseconds between each attempt. If all retries fail, the last error is
|
|
93
|
+
* wrapped in a new `Error` with a descriptive message, including:
|
|
94
|
+
* - Function name (or string representation truncated to `MaxFuncLength` chars if anonymous)
|
|
95
|
+
* - Number of retries attempted
|
|
96
|
+
* - Original error message
|
|
97
|
+
*
|
|
98
|
+
* Error handling is fully type-safe:
|
|
99
|
+
* - If the error is an ProtocolError (checked via `isProtocolError`), its
|
|
100
|
+
* message is used.
|
|
101
|
+
* - If the error is a standard `Error`, its `message` is used.
|
|
102
|
+
* - Otherwise, the error is converted to a string.
|
|
103
|
+
*
|
|
104
|
+
* @typeParam T - The return type of the function being retried.
|
|
105
|
+
* @param fn - A lambda or bound function returning a `Promise<T>`. The
|
|
106
|
+
* function should contain the logic you want to retry.
|
|
107
|
+
* @param maxRetries - Maximum number of retry attempts. Default: `MaxRetries`.
|
|
108
|
+
* @param interval - Delay between retries in milliseconds. Default: `MaxRetryInterval`.
|
|
109
|
+
* @returns A `Promise` resolving to the return value of `fn` if successful.
|
|
110
|
+
* @throws Error If all retry attempts fail, throws a new Error containing
|
|
111
|
+
* information about the function and the last error.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* const deployment = await waitFor(() => api.inspectByName('my-suffix'));
|
|
116
|
+
* ```
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* const result = await waitFor(
|
|
121
|
+
* () => api.deploy(name, env, plan, resourceType)
|
|
122
|
+
* );
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
export declare const waitFor: <T>(fn: () => Promise<T>, maxRetries?: number, interval?: number) => Promise<T>;
|
package/dist/protocol.js
CHANGED
|
@@ -18,14 +18,40 @@
|
|
|
18
18
|
branchList: get the branches of a repository
|
|
19
19
|
fileList: get files of a repository by branch
|
|
20
20
|
*/
|
|
21
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
|
24
|
+
}) : (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
o[k2] = m[k];
|
|
27
|
+
}));
|
|
28
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
+
}) : function(o, v) {
|
|
31
|
+
o["default"] = v;
|
|
32
|
+
});
|
|
33
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
21
40
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
22
41
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
23
42
|
};
|
|
24
43
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
-
exports.ResourceType = exports.isProtocolError = void 0;
|
|
26
|
-
const axios_1 =
|
|
44
|
+
exports.waitFor = exports.MaxFuncLength = exports.MaxRetryInterval = exports.MaxRetries = exports.InvokeType = exports.ResourceType = exports.ProtocolError = exports.isProtocolError = void 0;
|
|
45
|
+
const axios_1 = __importStar(require("axios"));
|
|
46
|
+
Object.defineProperty(exports, "ProtocolError", { enumerable: true, get: function () { return axios_1.AxiosError; } });
|
|
27
47
|
const form_data_1 = __importDefault(require("form-data"));
|
|
48
|
+
const url_1 = require("url");
|
|
28
49
|
const deployment_1 = require("./deployment");
|
|
50
|
+
/**
|
|
51
|
+
* Type guard for protocol-specific errors (Axios errors in this case).
|
|
52
|
+
* @param err - The unknown error to check.
|
|
53
|
+
* @returns True if the error is an ProtocolError, false otherwise.
|
|
54
|
+
*/
|
|
29
55
|
const isProtocolError = (err) => axios_1.default.isAxiosError(err);
|
|
30
56
|
exports.isProtocolError = isProtocolError;
|
|
31
57
|
var ResourceType;
|
|
@@ -33,27 +59,36 @@ var ResourceType;
|
|
|
33
59
|
ResourceType["Package"] = "Package";
|
|
34
60
|
ResourceType["Repository"] = "Repository";
|
|
35
61
|
})(ResourceType = exports.ResourceType || (exports.ResourceType = {}));
|
|
62
|
+
var InvokeType;
|
|
63
|
+
(function (InvokeType) {
|
|
64
|
+
InvokeType["Call"] = "call";
|
|
65
|
+
InvokeType["Await"] = "await";
|
|
66
|
+
})(InvokeType = exports.InvokeType || (exports.InvokeType = {}));
|
|
36
67
|
exports.default = (token, baseURL) => {
|
|
68
|
+
const getURL = (path) => new url_1.URL(path, baseURL).toString();
|
|
69
|
+
const getConfig = (headers = {}) => {
|
|
70
|
+
return {
|
|
71
|
+
headers: {
|
|
72
|
+
Authorization: 'jwt ' + token,
|
|
73
|
+
...headers
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
};
|
|
37
77
|
const api = {
|
|
38
78
|
refresh: () => axios_1.default
|
|
39
|
-
.get(
|
|
40
|
-
headers: { Authorization: 'jwt ' + token }
|
|
41
|
-
})
|
|
79
|
+
.get(getURL('/api/account/refresh-token'), getConfig())
|
|
42
80
|
.then(res => res.data),
|
|
81
|
+
ready: () => axios_1.default
|
|
82
|
+
.get(getURL('/api/readiness'), getConfig())
|
|
83
|
+
.then(res => res.status == 200),
|
|
43
84
|
validate: () => axios_1.default
|
|
44
|
-
.get(
|
|
45
|
-
headers: { Authorization: 'jwt ' + token }
|
|
46
|
-
})
|
|
85
|
+
.get(getURL('/validate'), getConfig())
|
|
47
86
|
.then(res => res.data),
|
|
48
87
|
deployEnabled: () => axios_1.default
|
|
49
|
-
.get(
|
|
50
|
-
headers: { Authorization: 'jwt ' + token }
|
|
51
|
-
})
|
|
88
|
+
.get(getURL('/api/account/deploy-enabled'), getConfig())
|
|
52
89
|
.then(res => res.data),
|
|
53
90
|
listSubscriptions: async () => {
|
|
54
|
-
const res = await axios_1.default.get(
|
|
55
|
-
headers: { Authorization: 'jwt ' + token }
|
|
56
|
-
});
|
|
91
|
+
const res = await axios_1.default.get(getURL('/api/billing/list-subscriptions'), getConfig());
|
|
57
92
|
const subscriptions = {};
|
|
58
93
|
for (const id of res.data) {
|
|
59
94
|
if (subscriptions[id] === undefined) {
|
|
@@ -65,18 +100,21 @@ exports.default = (token, baseURL) => {
|
|
|
65
100
|
}
|
|
66
101
|
return subscriptions;
|
|
67
102
|
},
|
|
68
|
-
listSubscriptionsDeploys:
|
|
69
|
-
.get(
|
|
70
|
-
headers: { Authorization: 'jwt ' + token }
|
|
71
|
-
})
|
|
103
|
+
listSubscriptionsDeploys: () => axios_1.default
|
|
104
|
+
.get(getURL('/api/billing/list-subscriptions-deploys'), getConfig())
|
|
72
105
|
.then(res => res.data),
|
|
73
|
-
inspect:
|
|
74
|
-
.get(
|
|
75
|
-
headers: { Authorization: 'jwt ' + token }
|
|
76
|
-
})
|
|
106
|
+
inspect: () => axios_1.default
|
|
107
|
+
.get(getURL('/api/inspect'), getConfig())
|
|
77
108
|
.then(res => res.data),
|
|
109
|
+
inspectByName: async (suffix) => {
|
|
110
|
+
const deployments = await api.inspect();
|
|
111
|
+
const deploy = deployments.find(deploy => deploy.suffix == suffix);
|
|
112
|
+
if (!deploy) {
|
|
113
|
+
throw new Error(`Deployment with suffix '${suffix}' not found`);
|
|
114
|
+
}
|
|
115
|
+
return deploy;
|
|
116
|
+
},
|
|
78
117
|
upload: async (name, blob, jsons = [], runners = []) => {
|
|
79
|
-
var _a, _b;
|
|
80
118
|
const fd = new form_data_1.default();
|
|
81
119
|
fd.append('id', name);
|
|
82
120
|
fd.append('type', 'application/x-zip-compressed');
|
|
@@ -86,70 +124,130 @@ exports.default = (token, baseURL) => {
|
|
|
86
124
|
filename: 'blob',
|
|
87
125
|
contentType: 'application/x-zip-compressed'
|
|
88
126
|
});
|
|
89
|
-
const res = await axios_1.default.post(
|
|
90
|
-
|
|
91
|
-
Authorization: 'jwt ' + token,
|
|
92
|
-
...((_b = (_a = fd.getHeaders) === null || _a === void 0 ? void 0 : _a.call(fd)) !== null && _b !== void 0 ? _b : {}) // operator chaining to make it compatible with frontend
|
|
93
|
-
}
|
|
94
|
-
});
|
|
127
|
+
const res = await axios_1.default.post(getURL('/api/package/create'), fd, getConfig(fd.getHeaders?.() ?? {}) // Operator chaining to make it compatible with frontend
|
|
128
|
+
);
|
|
95
129
|
return res.data;
|
|
96
130
|
},
|
|
97
131
|
add: (url, branch, jsons = []) => axios_1.default
|
|
98
|
-
.post(
|
|
132
|
+
.post(getURL('/api/repository/add'), {
|
|
99
133
|
url,
|
|
100
134
|
branch,
|
|
101
135
|
jsons
|
|
102
|
-
},
|
|
103
|
-
|
|
104
|
-
})
|
|
105
|
-
.then((res) => res.data),
|
|
136
|
+
}, getConfig())
|
|
137
|
+
.then(res => res.data),
|
|
106
138
|
branchList: (url) => axios_1.default
|
|
107
|
-
.post(
|
|
139
|
+
.post(getURL('/api/repository/branchlist'), {
|
|
108
140
|
url
|
|
109
|
-
},
|
|
110
|
-
|
|
111
|
-
})
|
|
112
|
-
.then((res) => res.data),
|
|
141
|
+
}, getConfig())
|
|
142
|
+
.then(res => res.data),
|
|
113
143
|
deploy: (name, env, plan, resourceType, release = Date.now().toString(16), version = 'v1') => axios_1.default
|
|
114
|
-
.post(
|
|
144
|
+
.post(getURL('/api/deploy/create'), {
|
|
115
145
|
resourceType,
|
|
116
146
|
suffix: name,
|
|
117
147
|
release,
|
|
118
148
|
env,
|
|
119
149
|
plan,
|
|
120
150
|
version
|
|
121
|
-
},
|
|
122
|
-
headers: { Authorization: 'jwt ' + token }
|
|
123
|
-
})
|
|
151
|
+
}, getConfig())
|
|
124
152
|
.then(res => res.data),
|
|
125
153
|
deployDelete: (prefix, suffix, version = 'v1') => axios_1.default
|
|
126
|
-
.post(
|
|
154
|
+
.post(getURL('/api/deploy/delete'), {
|
|
127
155
|
prefix,
|
|
128
156
|
suffix,
|
|
129
157
|
version
|
|
130
|
-
},
|
|
131
|
-
headers: { Authorization: 'jwt ' + token }
|
|
132
|
-
})
|
|
158
|
+
}, getConfig())
|
|
133
159
|
.then(res => res.data),
|
|
134
160
|
logs: (container, type = deployment_1.LogType.Deploy, suffix, prefix, version = 'v1') => axios_1.default
|
|
135
|
-
.post(
|
|
161
|
+
.post(getURL('/api/deploy/logs'), {
|
|
136
162
|
container,
|
|
137
163
|
type,
|
|
138
164
|
suffix,
|
|
139
165
|
prefix,
|
|
140
166
|
version
|
|
141
|
-
},
|
|
142
|
-
headers: { Authorization: 'jwt ' + token }
|
|
143
|
-
})
|
|
167
|
+
}, getConfig())
|
|
144
168
|
.then(res => res.data),
|
|
145
169
|
fileList: (url, branch) => axios_1.default
|
|
146
|
-
.post(
|
|
170
|
+
.post(getURL('/api/repository/filelist'), {
|
|
147
171
|
url,
|
|
148
172
|
branch
|
|
149
|
-
},
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
173
|
+
}, getConfig())
|
|
174
|
+
.then(res => res.data['files']),
|
|
175
|
+
invoke: (type, prefix, suffix, version = 'v1', name, args) => {
|
|
176
|
+
const url = getURL(`/${prefix}/${suffix}/${version}/${type}/${name}`);
|
|
177
|
+
const config = getConfig();
|
|
178
|
+
const req = args === undefined
|
|
179
|
+
? axios_1.default.get(url, config)
|
|
180
|
+
: axios_1.default.post(url, args, config);
|
|
181
|
+
return req.then(res => res.data);
|
|
182
|
+
},
|
|
183
|
+
call: (prefix, suffix, version = 'v1', name, args) => api.invoke(InvokeType.Call, prefix, suffix, version, name, args),
|
|
184
|
+
await: (prefix, suffix, version = 'v1', name, args) => api.invoke(InvokeType.Await, prefix, suffix, version, name, args)
|
|
153
185
|
};
|
|
154
186
|
return api;
|
|
155
187
|
};
|
|
188
|
+
exports.MaxRetries = 30;
|
|
189
|
+
exports.MaxRetryInterval = 2000;
|
|
190
|
+
exports.MaxFuncLength = 64;
|
|
191
|
+
/**
|
|
192
|
+
* Executes an asynchronous function with automatic retry logic.
|
|
193
|
+
*
|
|
194
|
+
* The function will be retried up to `maxRetries` times, waiting `interval`
|
|
195
|
+
* milliseconds between each attempt. If all retries fail, the last error is
|
|
196
|
+
* wrapped in a new `Error` with a descriptive message, including:
|
|
197
|
+
* - Function name (or string representation truncated to `MaxFuncLength` chars if anonymous)
|
|
198
|
+
* - Number of retries attempted
|
|
199
|
+
* - Original error message
|
|
200
|
+
*
|
|
201
|
+
* Error handling is fully type-safe:
|
|
202
|
+
* - If the error is an ProtocolError (checked via `isProtocolError`), its
|
|
203
|
+
* message is used.
|
|
204
|
+
* - If the error is a standard `Error`, its `message` is used.
|
|
205
|
+
* - Otherwise, the error is converted to a string.
|
|
206
|
+
*
|
|
207
|
+
* @typeParam T - The return type of the function being retried.
|
|
208
|
+
* @param fn - A lambda or bound function returning a `Promise<T>`. The
|
|
209
|
+
* function should contain the logic you want to retry.
|
|
210
|
+
* @param maxRetries - Maximum number of retry attempts. Default: `MaxRetries`.
|
|
211
|
+
* @param interval - Delay between retries in milliseconds. Default: `MaxRetryInterval`.
|
|
212
|
+
* @returns A `Promise` resolving to the return value of `fn` if successful.
|
|
213
|
+
* @throws Error If all retry attempts fail, throws a new Error containing
|
|
214
|
+
* information about the function and the last error.
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```ts
|
|
218
|
+
* const deployment = await waitFor(() => api.inspectByName('my-suffix'));
|
|
219
|
+
* ```
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```ts
|
|
223
|
+
* const result = await waitFor(
|
|
224
|
+
* () => api.deploy(name, env, plan, resourceType)
|
|
225
|
+
* );
|
|
226
|
+
* ```
|
|
227
|
+
*/
|
|
228
|
+
const waitFor = async (fn, maxRetries = exports.MaxRetries, interval = exports.MaxRetryInterval) => {
|
|
229
|
+
let retry = 0;
|
|
230
|
+
for (;;) {
|
|
231
|
+
try {
|
|
232
|
+
return await fn();
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
retry++;
|
|
236
|
+
if (retry >= maxRetries) {
|
|
237
|
+
const fnStr = fn.toString();
|
|
238
|
+
const func = fn.name ||
|
|
239
|
+
(fnStr.length > exports.MaxFuncLength
|
|
240
|
+
? fnStr.slice(0, exports.MaxFuncLength) + '...'
|
|
241
|
+
: fnStr);
|
|
242
|
+
const message = exports.isProtocolError(error)
|
|
243
|
+
? error.message
|
|
244
|
+
: error instanceof Error
|
|
245
|
+
? error.message
|
|
246
|
+
: String(error);
|
|
247
|
+
throw new Error(`Failed to execute '${func}' after ${maxRetries} retries: ${message}`);
|
|
248
|
+
}
|
|
249
|
+
await new Promise(r => setTimeout(r, interval));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
exports.waitFor = waitFor;
|
package/dist/token.js
CHANGED
|
@@ -11,6 +11,6 @@ const expiresIn = (token) => {
|
|
|
11
11
|
return 0;
|
|
12
12
|
}
|
|
13
13
|
const now = Date.now() / 1000;
|
|
14
|
-
return new Date((
|
|
14
|
+
return new Date((decoded?.['exp'] || now) * 1000).getTime() - now * 1000;
|
|
15
15
|
};
|
|
16
16
|
exports.expiresIn = expiresIn;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metacall/protocol",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.27",
|
|
4
4
|
"description": "Tool for deploying into MetaCall FaaS platform.",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./*": "./dist/*.js",
|
|
@@ -85,15 +85,15 @@
|
|
|
85
85
|
}
|
|
86
86
|
},
|
|
87
87
|
"dependencies": {
|
|
88
|
-
"
|
|
89
|
-
"@types/jsonwebtoken": "^8.5.8",
|
|
90
|
-
"axios": "^0.21.0",
|
|
88
|
+
"axios": "^1.13.5",
|
|
91
89
|
"form-data": "^3.0.0",
|
|
92
90
|
"ignore-walk": "^3.0.4",
|
|
93
91
|
"jsonwebtoken": "^9.0.0"
|
|
94
92
|
},
|
|
95
93
|
"devDependencies": {
|
|
96
94
|
"@types/express": "^4.17.13",
|
|
95
|
+
"@types/ignore-walk": "^4.0.0",
|
|
96
|
+
"@types/jsonwebtoken": "^8.5.8",
|
|
97
97
|
"@types/mocha": "^8.2.3",
|
|
98
98
|
"@types/node": "^14.14.7",
|
|
99
99
|
"@types/swagger-ui-express": "^4.1.3",
|
|
@@ -111,4 +111,4 @@
|
|
|
111
111
|
"typescript": "4.3.2",
|
|
112
112
|
"yamljs": "^0.3.0"
|
|
113
113
|
}
|
|
114
|
-
}
|
|
114
|
+
}
|
package/src/index.ts
CHANGED
package/src/language.ts
CHANGED
|
@@ -5,14 +5,56 @@
|
|
|
5
5
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { basename } from 'path';
|
|
8
9
|
import { LanguageId } from './deployment';
|
|
9
10
|
|
|
11
|
+
export type Runner = 'nodejs' | 'python' | 'ruby' | 'csharp';
|
|
12
|
+
|
|
13
|
+
export interface RunnerInfo {
|
|
14
|
+
id: Runner;
|
|
15
|
+
languageId: LanguageId;
|
|
16
|
+
filePatterns: RegExp[];
|
|
17
|
+
installCommand: string;
|
|
18
|
+
displayName: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const Runners: Record<Runner, RunnerInfo> = {
|
|
22
|
+
nodejs: {
|
|
23
|
+
id: 'nodejs',
|
|
24
|
+
languageId: 'node',
|
|
25
|
+
filePatterns: [/^package\.json$/],
|
|
26
|
+
installCommand: 'npm install',
|
|
27
|
+
displayName: 'NPM'
|
|
28
|
+
},
|
|
29
|
+
python: {
|
|
30
|
+
id: 'python',
|
|
31
|
+
languageId: 'py',
|
|
32
|
+
filePatterns: [/^requirements\.txt$/],
|
|
33
|
+
installCommand: 'pip install -r requirements.txt',
|
|
34
|
+
displayName: 'Pip'
|
|
35
|
+
},
|
|
36
|
+
ruby: {
|
|
37
|
+
id: 'ruby',
|
|
38
|
+
languageId: 'rb',
|
|
39
|
+
filePatterns: [/^Gemfile$/],
|
|
40
|
+
installCommand: 'bundle install',
|
|
41
|
+
displayName: 'Gem'
|
|
42
|
+
},
|
|
43
|
+
csharp: {
|
|
44
|
+
id: 'csharp',
|
|
45
|
+
languageId: 'cs',
|
|
46
|
+
filePatterns: [/^project\.json$/, /\.csproj$/],
|
|
47
|
+
installCommand: 'dotnet restore',
|
|
48
|
+
displayName: 'NuGet'
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
10
52
|
interface Language {
|
|
11
53
|
tag: string; // Tag which corresponds to language_id in metacall.json
|
|
12
54
|
displayName: string; // Name for displaying the language
|
|
13
55
|
hexColor: string; // Color for displaying the language related things
|
|
14
56
|
fileExtRegex: RegExp; // Regex for selecting the metacall.json scripts field
|
|
15
|
-
runnerName?:
|
|
57
|
+
runnerName?: Runner; // Id of the runner
|
|
16
58
|
runnerFilesRegexes: RegExp[]; // Regex for generating the runners list
|
|
17
59
|
}
|
|
18
60
|
|
|
@@ -23,7 +65,7 @@ export const Languages: Record<LanguageId, Language> = {
|
|
|
23
65
|
hexColor: '#953dac',
|
|
24
66
|
fileExtRegex: /^cs$/,
|
|
25
67
|
runnerName: 'csharp',
|
|
26
|
-
runnerFilesRegexes:
|
|
68
|
+
runnerFilesRegexes: Runners.csharp.filePatterns
|
|
27
69
|
},
|
|
28
70
|
py: {
|
|
29
71
|
tag: 'py',
|
|
@@ -31,7 +73,7 @@ export const Languages: Record<LanguageId, Language> = {
|
|
|
31
73
|
hexColor: '#ffd43b',
|
|
32
74
|
fileExtRegex: /^py$/,
|
|
33
75
|
runnerName: 'python',
|
|
34
|
-
runnerFilesRegexes:
|
|
76
|
+
runnerFilesRegexes: Runners.python.filePatterns
|
|
35
77
|
},
|
|
36
78
|
rb: {
|
|
37
79
|
tag: 'rb',
|
|
@@ -39,7 +81,7 @@ export const Languages: Record<LanguageId, Language> = {
|
|
|
39
81
|
hexColor: '#e53935',
|
|
40
82
|
fileExtRegex: /^rb$/,
|
|
41
83
|
runnerName: 'ruby',
|
|
42
|
-
runnerFilesRegexes:
|
|
84
|
+
runnerFilesRegexes: Runners.ruby.filePatterns
|
|
43
85
|
},
|
|
44
86
|
node: {
|
|
45
87
|
tag: 'node',
|
|
@@ -47,7 +89,7 @@ export const Languages: Record<LanguageId, Language> = {
|
|
|
47
89
|
hexColor: '#3c873a',
|
|
48
90
|
fileExtRegex: /^js$/,
|
|
49
91
|
runnerName: 'nodejs',
|
|
50
|
-
runnerFilesRegexes:
|
|
92
|
+
runnerFilesRegexes: Runners.nodejs.filePatterns
|
|
51
93
|
},
|
|
52
94
|
ts: {
|
|
53
95
|
tag: 'ts',
|
|
@@ -55,7 +97,7 @@ export const Languages: Record<LanguageId, Language> = {
|
|
|
55
97
|
hexColor: '#007acc',
|
|
56
98
|
fileExtRegex: /^(ts|tsx)$/,
|
|
57
99
|
runnerName: 'nodejs',
|
|
58
|
-
runnerFilesRegexes:
|
|
100
|
+
runnerFilesRegexes: Runners.nodejs.filePatterns
|
|
59
101
|
},
|
|
60
102
|
file: {
|
|
61
103
|
tag: 'file',
|
|
@@ -94,12 +136,26 @@ export const DisplayNameToLanguageId: Record<string, LanguageId> = Object.keys(
|
|
|
94
136
|
);
|
|
95
137
|
|
|
96
138
|
export const RunnerToDisplayName = (runner: string): string => {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
139
|
+
const match = Runners[runner as Runner];
|
|
140
|
+
|
|
141
|
+
return match ? match.displayName : 'Build';
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const detectRunnersFromFiles = (files: string[]): Runner[] => {
|
|
145
|
+
const runners = new Set<Runner>();
|
|
146
|
+
|
|
147
|
+
for (const file of files) {
|
|
148
|
+
const fileName = basename(file);
|
|
149
|
+
|
|
150
|
+
for (const runner of Object.values(Runners)) {
|
|
151
|
+
for (const pattern of runner.filePatterns) {
|
|
152
|
+
if (pattern.exec(fileName)) {
|
|
153
|
+
runners.add(runner.id);
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
103
159
|
|
|
104
|
-
return
|
|
160
|
+
return Array.from(runners);
|
|
105
161
|
};
|
package/src/package.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import walk from 'ignore-walk';
|
|
16
16
|
import { basename, extname } from 'path';
|
|
17
17
|
import { LanguageId, MetaCallJSON } from './deployment';
|
|
18
|
-
import { Languages } from './language';
|
|
18
|
+
import { detectRunnersFromFiles, Languages, Runner } from './language';
|
|
19
19
|
|
|
20
20
|
export const findFilesPath = async (
|
|
21
21
|
path: string = process.cwd(),
|
|
@@ -30,29 +30,20 @@ export const findFilesPath = async (
|
|
|
30
30
|
})
|
|
31
31
|
).filter(x => !x.startsWith('.git'));
|
|
32
32
|
|
|
33
|
-
const pathIsMetaCallJson = (path: string): boolean =>
|
|
33
|
+
export const pathIsMetaCallJson = (path: string): boolean =>
|
|
34
34
|
!!/^metacall(-.+)?\.json$/.exec(basename(path));
|
|
35
35
|
|
|
36
36
|
export const findMetaCallJsons = (files: string[]): string[] =>
|
|
37
37
|
files.filter(pathIsMetaCallJson);
|
|
38
38
|
|
|
39
|
-
export const findRunners = (files: string[]): Set<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
for (const file of files) {
|
|
43
|
-
const fileName = basename(file);
|
|
44
|
-
for (const langId of Object.keys(Languages)) {
|
|
45
|
-
const lang = Languages[langId as LanguageId];
|
|
46
|
-
for (const re of lang.runnerFilesRegexes) {
|
|
47
|
-
if (re.exec(fileName) && lang.runnerName) {
|
|
48
|
-
runners.add(lang.runnerName);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
39
|
+
export const findRunners = (files: string[]): Set<Runner> =>
|
|
40
|
+
new Set<Runner>(detectRunnersFromFiles(files));
|
|
53
41
|
|
|
54
|
-
|
|
55
|
-
|
|
42
|
+
export const detectRunners = async (
|
|
43
|
+
path: string = process.cwd(),
|
|
44
|
+
ignoreFiles: string[] = ['.gitignore']
|
|
45
|
+
): Promise<Runner[]> =>
|
|
46
|
+
detectRunnersFromFiles(await findFilesPath(path, ignoreFiles));
|
|
56
47
|
|
|
57
48
|
export enum PackageError {
|
|
58
49
|
Empty = 'No files found in the current folder',
|
|
@@ -64,7 +55,7 @@ interface PackageDescriptor {
|
|
|
64
55
|
error: PackageError;
|
|
65
56
|
files: string[];
|
|
66
57
|
jsons: string[];
|
|
67
|
-
runners:
|
|
58
|
+
runners: Runner[];
|
|
68
59
|
}
|
|
69
60
|
|
|
70
61
|
const NullPackage: PackageDescriptor = {
|
package/src/protocol.ts
CHANGED
|
@@ -18,11 +18,18 @@
|
|
|
18
18
|
fileList: get files of a repository by branch
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import axios, { AxiosError,
|
|
21
|
+
import axios, { AxiosError, AxiosRequestConfig } from 'axios';
|
|
22
22
|
import FormData from 'form-data';
|
|
23
|
+
import { URL } from 'url';
|
|
23
24
|
import { Create, Deployment, LogType, MetaCallJSON } from './deployment';
|
|
24
25
|
import { Plans } from './plan';
|
|
26
|
+
import { ProtocolError } from './protocol';
|
|
25
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Type guard for protocol-specific errors (Axios errors in this case).
|
|
30
|
+
* @param err - The unknown error to check.
|
|
31
|
+
* @returns True if the error is an ProtocolError, false otherwise.
|
|
32
|
+
*/
|
|
26
33
|
export const isProtocolError = (err: unknown): boolean =>
|
|
27
34
|
axios.isAxiosError(err);
|
|
28
35
|
|
|
@@ -50,13 +57,50 @@ export interface Branches {
|
|
|
50
57
|
branches: [string];
|
|
51
58
|
}
|
|
52
59
|
|
|
60
|
+
export enum InvokeType {
|
|
61
|
+
Call = 'call',
|
|
62
|
+
Await = 'await'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface DeployCreateRequest {
|
|
66
|
+
suffix: string;
|
|
67
|
+
resourceType: ResourceType;
|
|
68
|
+
release: string;
|
|
69
|
+
env: { name: string; value: string }[];
|
|
70
|
+
plan: Plans;
|
|
71
|
+
version: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface DeployDeleteRequest {
|
|
75
|
+
prefix: string;
|
|
76
|
+
suffix: string;
|
|
77
|
+
version: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface RepositoryAddRequest {
|
|
81
|
+
url: string;
|
|
82
|
+
branch: string;
|
|
83
|
+
jsons: MetaCallJSON[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface RepositoryBranchListRequest {
|
|
87
|
+
url: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface RepositoryFileListRequest {
|
|
91
|
+
url: string;
|
|
92
|
+
branch: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
53
95
|
export interface API {
|
|
54
96
|
refresh(): Promise<string>;
|
|
97
|
+
ready(): Promise<boolean>;
|
|
55
98
|
validate(): Promise<boolean>;
|
|
56
99
|
deployEnabled(): Promise<boolean>;
|
|
57
100
|
listSubscriptions(): Promise<SubscriptionMap>;
|
|
58
101
|
listSubscriptionsDeploys(): Promise<SubscriptionDeploy[]>;
|
|
59
102
|
inspect(): Promise<Deployment[]>;
|
|
103
|
+
inspectByName(suffix: string): Promise<Deployment>;
|
|
60
104
|
upload(
|
|
61
105
|
name: string,
|
|
62
106
|
blob: unknown,
|
|
@@ -90,37 +134,69 @@ export interface API {
|
|
|
90
134
|
): Promise<string>;
|
|
91
135
|
branchList(url: string): Promise<Branches>;
|
|
92
136
|
fileList(url: string, branch: string): Promise<string[]>;
|
|
137
|
+
invoke<Result, Args = unknown>(
|
|
138
|
+
type: InvokeType,
|
|
139
|
+
prefix: string,
|
|
140
|
+
suffix: string,
|
|
141
|
+
version: string,
|
|
142
|
+
name: string,
|
|
143
|
+
args?: Args
|
|
144
|
+
): Promise<Result>;
|
|
145
|
+
call<Result, Args = unknown>(
|
|
146
|
+
prefix: string,
|
|
147
|
+
suffix: string,
|
|
148
|
+
version: string,
|
|
149
|
+
name: string,
|
|
150
|
+
args?: Args
|
|
151
|
+
): Promise<Result>;
|
|
152
|
+
await<Result, Args = unknown>(
|
|
153
|
+
prefix: string,
|
|
154
|
+
suffix: string,
|
|
155
|
+
version: string,
|
|
156
|
+
name: string,
|
|
157
|
+
args?: Args
|
|
158
|
+
): Promise<Result>;
|
|
93
159
|
}
|
|
94
160
|
|
|
95
161
|
export default (token: string, baseURL: string): API => {
|
|
162
|
+
const getURL = (path: string): string => new URL(path, baseURL).toString();
|
|
163
|
+
const getConfig = (headers = {}): AxiosRequestConfig => {
|
|
164
|
+
return {
|
|
165
|
+
headers: {
|
|
166
|
+
Authorization: 'jwt ' + token,
|
|
167
|
+
...headers
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
|
|
96
172
|
const api: API = {
|
|
97
173
|
refresh: (): Promise<string> =>
|
|
98
174
|
axios
|
|
99
|
-
.get<string>(
|
|
100
|
-
headers: { Authorization: 'jwt ' + token }
|
|
101
|
-
})
|
|
175
|
+
.get<string>(getURL('/api/account/refresh-token'), getConfig())
|
|
102
176
|
.then(res => res.data),
|
|
103
177
|
|
|
178
|
+
ready: (): Promise<boolean> =>
|
|
179
|
+
axios
|
|
180
|
+
.get<boolean>(getURL('/api/readiness'), getConfig())
|
|
181
|
+
.then(res => res.status == 200),
|
|
182
|
+
|
|
104
183
|
validate: (): Promise<boolean> =>
|
|
105
184
|
axios
|
|
106
|
-
.get<boolean>(
|
|
107
|
-
headers: { Authorization: 'jwt ' + token }
|
|
108
|
-
})
|
|
185
|
+
.get<boolean>(getURL('/validate'), getConfig())
|
|
109
186
|
.then(res => res.data),
|
|
110
187
|
|
|
111
188
|
deployEnabled: (): Promise<boolean> =>
|
|
112
189
|
axios
|
|
113
|
-
.get<boolean>(
|
|
114
|
-
|
|
115
|
-
|
|
190
|
+
.get<boolean>(
|
|
191
|
+
getURL('/api/account/deploy-enabled'),
|
|
192
|
+
getConfig()
|
|
193
|
+
)
|
|
116
194
|
.then(res => res.data),
|
|
117
195
|
|
|
118
196
|
listSubscriptions: async (): Promise<SubscriptionMap> => {
|
|
119
197
|
const res = await axios.get<string[]>(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
headers: { Authorization: 'jwt ' + token }
|
|
123
|
-
}
|
|
198
|
+
getURL('/api/billing/list-subscriptions'),
|
|
199
|
+
getConfig()
|
|
124
200
|
);
|
|
125
201
|
|
|
126
202
|
const subscriptions: SubscriptionMap = {};
|
|
@@ -136,23 +212,31 @@ export default (token: string, baseURL: string): API => {
|
|
|
136
212
|
return subscriptions;
|
|
137
213
|
},
|
|
138
214
|
|
|
139
|
-
listSubscriptionsDeploys:
|
|
215
|
+
listSubscriptionsDeploys: (): Promise<SubscriptionDeploy[]> =>
|
|
140
216
|
axios
|
|
141
217
|
.get<SubscriptionDeploy[]>(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
headers: { Authorization: 'jwt ' + token }
|
|
145
|
-
}
|
|
218
|
+
getURL('/api/billing/list-subscriptions-deploys'),
|
|
219
|
+
getConfig()
|
|
146
220
|
)
|
|
147
221
|
.then(res => res.data),
|
|
148
222
|
|
|
149
|
-
inspect:
|
|
223
|
+
inspect: (): Promise<Deployment[]> =>
|
|
150
224
|
axios
|
|
151
|
-
.get<Deployment[]>(
|
|
152
|
-
headers: { Authorization: 'jwt ' + token }
|
|
153
|
-
})
|
|
225
|
+
.get<Deployment[]>(getURL('/api/inspect'), getConfig())
|
|
154
226
|
.then(res => res.data),
|
|
155
227
|
|
|
228
|
+
inspectByName: async (suffix: string): Promise<Deployment> => {
|
|
229
|
+
const deployments = await api.inspect();
|
|
230
|
+
|
|
231
|
+
const deploy = deployments.find(deploy => deploy.suffix == suffix);
|
|
232
|
+
|
|
233
|
+
if (!deploy) {
|
|
234
|
+
throw new Error(`Deployment with suffix '${suffix}' not found`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return deploy;
|
|
238
|
+
},
|
|
239
|
+
|
|
156
240
|
upload: async (
|
|
157
241
|
name: string,
|
|
158
242
|
blob: unknown,
|
|
@@ -169,14 +253,9 @@ export default (token: string, baseURL: string): API => {
|
|
|
169
253
|
contentType: 'application/x-zip-compressed'
|
|
170
254
|
});
|
|
171
255
|
const res = await axios.post<string>(
|
|
172
|
-
|
|
256
|
+
getURL('/api/package/create'),
|
|
173
257
|
fd,
|
|
174
|
-
{
|
|
175
|
-
headers: {
|
|
176
|
-
Authorization: 'jwt ' + token,
|
|
177
|
-
...(fd.getHeaders?.() ?? {}) // operator chaining to make it compatible with frontend
|
|
178
|
-
}
|
|
179
|
-
}
|
|
258
|
+
getConfig(fd.getHeaders?.() ?? {}) // Operator chaining to make it compatible with frontend
|
|
180
259
|
);
|
|
181
260
|
return res.data;
|
|
182
261
|
},
|
|
@@ -186,30 +265,26 @@ export default (token: string, baseURL: string): API => {
|
|
|
186
265
|
jsons: MetaCallJSON[] = []
|
|
187
266
|
): Promise<AddResponse> =>
|
|
188
267
|
axios
|
|
189
|
-
.post<
|
|
190
|
-
|
|
268
|
+
.post<AddResponse>(
|
|
269
|
+
getURL('/api/repository/add'),
|
|
191
270
|
{
|
|
192
271
|
url,
|
|
193
272
|
branch,
|
|
194
273
|
jsons
|
|
195
274
|
},
|
|
196
|
-
|
|
197
|
-
headers: { Authorization: 'jwt ' + token }
|
|
198
|
-
}
|
|
275
|
+
getConfig()
|
|
199
276
|
)
|
|
200
|
-
.then(
|
|
277
|
+
.then(res => res.data),
|
|
201
278
|
branchList: (url: string): Promise<Branches> =>
|
|
202
279
|
axios
|
|
203
|
-
.post<
|
|
204
|
-
|
|
280
|
+
.post<Branches>(
|
|
281
|
+
getURL('/api/repository/branchlist'),
|
|
205
282
|
{
|
|
206
283
|
url
|
|
207
284
|
},
|
|
208
|
-
|
|
209
|
-
headers: { Authorization: 'jwt ' + token }
|
|
210
|
-
}
|
|
285
|
+
getConfig()
|
|
211
286
|
)
|
|
212
|
-
.then(
|
|
287
|
+
.then(res => res.data),
|
|
213
288
|
|
|
214
289
|
deploy: (
|
|
215
290
|
name: string,
|
|
@@ -221,7 +296,7 @@ export default (token: string, baseURL: string): API => {
|
|
|
221
296
|
): Promise<Create> =>
|
|
222
297
|
axios
|
|
223
298
|
.post<Create>(
|
|
224
|
-
|
|
299
|
+
getURL('/api/deploy/create'),
|
|
225
300
|
{
|
|
226
301
|
resourceType,
|
|
227
302
|
suffix: name,
|
|
@@ -230,9 +305,7 @@ export default (token: string, baseURL: string): API => {
|
|
|
230
305
|
plan,
|
|
231
306
|
version
|
|
232
307
|
},
|
|
233
|
-
|
|
234
|
-
headers: { Authorization: 'jwt ' + token }
|
|
235
|
-
}
|
|
308
|
+
getConfig()
|
|
236
309
|
)
|
|
237
310
|
.then(res => res.data),
|
|
238
311
|
|
|
@@ -243,15 +316,13 @@ export default (token: string, baseURL: string): API => {
|
|
|
243
316
|
): Promise<string> =>
|
|
244
317
|
axios
|
|
245
318
|
.post<string>(
|
|
246
|
-
|
|
319
|
+
getURL('/api/deploy/delete'),
|
|
247
320
|
{
|
|
248
321
|
prefix,
|
|
249
322
|
suffix,
|
|
250
323
|
version
|
|
251
324
|
},
|
|
252
|
-
|
|
253
|
-
headers: { Authorization: 'jwt ' + token }
|
|
254
|
-
}
|
|
325
|
+
getConfig()
|
|
255
326
|
)
|
|
256
327
|
.then(res => res.data),
|
|
257
328
|
|
|
@@ -264,7 +335,7 @@ export default (token: string, baseURL: string): API => {
|
|
|
264
335
|
): Promise<string> =>
|
|
265
336
|
axios
|
|
266
337
|
.post<string>(
|
|
267
|
-
|
|
338
|
+
getURL('/api/deploy/logs'),
|
|
268
339
|
{
|
|
269
340
|
container,
|
|
270
341
|
type,
|
|
@@ -272,26 +343,137 @@ export default (token: string, baseURL: string): API => {
|
|
|
272
343
|
prefix,
|
|
273
344
|
version
|
|
274
345
|
},
|
|
275
|
-
|
|
276
|
-
headers: { Authorization: 'jwt ' + token }
|
|
277
|
-
}
|
|
346
|
+
getConfig()
|
|
278
347
|
)
|
|
279
348
|
.then(res => res.data),
|
|
280
349
|
|
|
281
350
|
fileList: (url: string, branch: string): Promise<string[]> =>
|
|
282
351
|
axios
|
|
283
352
|
.post<{ [k: string]: string[] }>(
|
|
284
|
-
|
|
353
|
+
getURL('/api/repository/filelist'),
|
|
285
354
|
{
|
|
286
355
|
url,
|
|
287
356
|
branch
|
|
288
357
|
},
|
|
289
|
-
|
|
290
|
-
headers: { Authorization: 'jwt ' + token }
|
|
291
|
-
}
|
|
358
|
+
getConfig()
|
|
292
359
|
)
|
|
293
|
-
.then(res => res.data['files'])
|
|
360
|
+
.then(res => res.data['files']),
|
|
361
|
+
|
|
362
|
+
invoke: <Result, Args = unknown>(
|
|
363
|
+
type: InvokeType,
|
|
364
|
+
prefix: string,
|
|
365
|
+
suffix: string,
|
|
366
|
+
version = 'v1',
|
|
367
|
+
name: string,
|
|
368
|
+
args?: Args
|
|
369
|
+
): Promise<Result> => {
|
|
370
|
+
const url = getURL(
|
|
371
|
+
`/${prefix}/${suffix}/${version}/${type}/${name}`
|
|
372
|
+
);
|
|
373
|
+
const config = getConfig();
|
|
374
|
+
|
|
375
|
+
const req =
|
|
376
|
+
args === undefined
|
|
377
|
+
? axios.get<Result>(url, config)
|
|
378
|
+
: axios.post<Result>(url, args, config);
|
|
379
|
+
|
|
380
|
+
return req.then(res => res.data);
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
call: <Result, Args = unknown>(
|
|
384
|
+
prefix: string,
|
|
385
|
+
suffix: string,
|
|
386
|
+
version = 'v1',
|
|
387
|
+
name: string,
|
|
388
|
+
args?: Args
|
|
389
|
+
): Promise<Result> =>
|
|
390
|
+
api.invoke(InvokeType.Call, prefix, suffix, version, name, args),
|
|
391
|
+
|
|
392
|
+
await: <Result, Args = unknown>(
|
|
393
|
+
prefix: string,
|
|
394
|
+
suffix: string,
|
|
395
|
+
version = 'v1',
|
|
396
|
+
name: string,
|
|
397
|
+
args?: Args
|
|
398
|
+
): Promise<Result> =>
|
|
399
|
+
api.invoke(InvokeType.Await, prefix, suffix, version, name, args)
|
|
294
400
|
};
|
|
295
401
|
|
|
296
402
|
return api;
|
|
297
403
|
};
|
|
404
|
+
|
|
405
|
+
export const MaxRetries = 30;
|
|
406
|
+
export const MaxRetryInterval = 2000;
|
|
407
|
+
export const MaxFuncLength = 64;
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Executes an asynchronous function with automatic retry logic.
|
|
411
|
+
*
|
|
412
|
+
* The function will be retried up to `maxRetries` times, waiting `interval`
|
|
413
|
+
* milliseconds between each attempt. If all retries fail, the last error is
|
|
414
|
+
* wrapped in a new `Error` with a descriptive message, including:
|
|
415
|
+
* - Function name (or string representation truncated to `MaxFuncLength` chars if anonymous)
|
|
416
|
+
* - Number of retries attempted
|
|
417
|
+
* - Original error message
|
|
418
|
+
*
|
|
419
|
+
* Error handling is fully type-safe:
|
|
420
|
+
* - If the error is an ProtocolError (checked via `isProtocolError`), its
|
|
421
|
+
* message is used.
|
|
422
|
+
* - If the error is a standard `Error`, its `message` is used.
|
|
423
|
+
* - Otherwise, the error is converted to a string.
|
|
424
|
+
*
|
|
425
|
+
* @typeParam T - The return type of the function being retried.
|
|
426
|
+
* @param fn - A lambda or bound function returning a `Promise<T>`. The
|
|
427
|
+
* function should contain the logic you want to retry.
|
|
428
|
+
* @param maxRetries - Maximum number of retry attempts. Default: `MaxRetries`.
|
|
429
|
+
* @param interval - Delay between retries in milliseconds. Default: `MaxRetryInterval`.
|
|
430
|
+
* @returns A `Promise` resolving to the return value of `fn` if successful.
|
|
431
|
+
* @throws Error If all retry attempts fail, throws a new Error containing
|
|
432
|
+
* information about the function and the last error.
|
|
433
|
+
*
|
|
434
|
+
* @example
|
|
435
|
+
* ```ts
|
|
436
|
+
* const deployment = await waitFor(() => api.inspectByName('my-suffix'));
|
|
437
|
+
* ```
|
|
438
|
+
*
|
|
439
|
+
* @example
|
|
440
|
+
* ```ts
|
|
441
|
+
* const result = await waitFor(
|
|
442
|
+
* () => api.deploy(name, env, plan, resourceType)
|
|
443
|
+
* );
|
|
444
|
+
* ```
|
|
445
|
+
*/
|
|
446
|
+
export const waitFor = async <T>(
|
|
447
|
+
fn: () => Promise<T>,
|
|
448
|
+
maxRetries: number = MaxRetries,
|
|
449
|
+
interval: number = MaxRetryInterval
|
|
450
|
+
): Promise<T> => {
|
|
451
|
+
let retry = 0;
|
|
452
|
+
|
|
453
|
+
for (;;) {
|
|
454
|
+
try {
|
|
455
|
+
return await fn();
|
|
456
|
+
} catch (error) {
|
|
457
|
+
retry++;
|
|
458
|
+
if (retry >= maxRetries) {
|
|
459
|
+
const fnStr = fn.toString();
|
|
460
|
+
const func =
|
|
461
|
+
fn.name ||
|
|
462
|
+
(fnStr.length > MaxFuncLength
|
|
463
|
+
? fnStr.slice(0, MaxFuncLength) + '...'
|
|
464
|
+
: fnStr);
|
|
465
|
+
const message = isProtocolError(error)
|
|
466
|
+
? (error as ProtocolError).message
|
|
467
|
+
: error instanceof Error
|
|
468
|
+
? error.message
|
|
469
|
+
: String(error);
|
|
470
|
+
|
|
471
|
+
throw new Error(
|
|
472
|
+
`Failed to execute '${func}' after ${maxRetries} retries: ${message}`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
await new Promise(r => setTimeout(r, interval));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
};
|
package/tsconfig.json
CHANGED