jitsu-cli 2.14.0-beta.8 → 2.14.0-beta.93
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/bin/jitsu +3 -0
- package/build.mts +39 -0
- package/compiled/package.json +16 -22
- package/compiled/src/commands/build.js +16 -24
- package/compiled/src/commands/config/handlers.js +292 -0
- package/compiled/src/commands/config/index.js +148 -0
- package/compiled/src/commands/config/resources.js +88 -0
- package/compiled/src/commands/default-workspace.js +39 -0
- package/compiled/src/commands/deploy.js +49 -17
- package/compiled/src/commands/spec.js +31 -0
- package/compiled/src/index.js +21 -2
- package/compiled/src/lib/api-client.js +52 -0
- package/compiled/src/lib/auth-file.js +67 -0
- package/compiled/src/lib/body-builder.js +53 -0
- package/compiled/src/lib/body-fields.js +47 -0
- package/compiled/src/lib/compiled-function.js +23 -21
- package/compiled/src/lib/dotted.js +34 -0
- package/compiled/src/lib/renderer.js +33 -0
- package/compiled/src/lib/spec.js +11 -0
- package/dist/main.js +46393 -92868
- package/dist/main.js.map +7 -1
- package/package.json +16 -22
- package/src/commands/build.ts +21 -26
- package/src/commands/config/handlers.ts +339 -0
- package/src/commands/config/index.ts +171 -0
- package/src/commands/config/resources.ts +110 -0
- package/src/commands/default-workspace.ts +44 -0
- package/src/commands/deploy.ts +90 -16
- package/src/commands/spec.ts +32 -0
- package/src/index.ts +34 -17
- package/src/lib/api-client.ts +64 -0
- package/src/lib/auth-file.ts +83 -0
- package/src/lib/body-builder.ts +68 -0
- package/src/lib/body-fields.ts +61 -0
- package/src/lib/compiled-function.ts +30 -23
- package/src/lib/dotted.ts +43 -0
- package/src/lib/renderer.ts +44 -0
- package/src/lib/spec.ts +32 -0
- package/dist/140.js +0 -452
- package/dist/140.js.map +0 -1
- package/webpack.config.cjs +0 -53
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export const resources = [
|
|
2
|
+
{
|
|
3
|
+
noun: "workspaces",
|
|
4
|
+
aliases: ["workspace"],
|
|
5
|
+
kind: "workspace",
|
|
6
|
+
description: "Workspaces accessible to the current user",
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
noun: "destinations",
|
|
10
|
+
aliases: ["destination", "dest"],
|
|
11
|
+
kind: "configObject",
|
|
12
|
+
type: "destination",
|
|
13
|
+
supportsTest: true,
|
|
14
|
+
description: "Destinations (warehouses, databases, services receiving events)",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
noun: "streams",
|
|
18
|
+
aliases: ["stream"],
|
|
19
|
+
kind: "configObject",
|
|
20
|
+
type: "stream",
|
|
21
|
+
supportsTest: true,
|
|
22
|
+
description: "Event streams (formerly known as sources)",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
noun: "functions",
|
|
26
|
+
aliases: ["function", "fn"],
|
|
27
|
+
kind: "configObject",
|
|
28
|
+
type: "function",
|
|
29
|
+
description: "User-defined functions (UDFs). For dev workflow see `jitsu deploy`.",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
noun: "services",
|
|
33
|
+
aliases: ["service"],
|
|
34
|
+
kind: "configObject",
|
|
35
|
+
type: "service",
|
|
36
|
+
supportsTest: true,
|
|
37
|
+
description: "External connector services (Airbyte protocol)",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
noun: "domains",
|
|
41
|
+
aliases: ["domain"],
|
|
42
|
+
kind: "configObject",
|
|
43
|
+
type: "domain",
|
|
44
|
+
description: "Custom ingestion domains",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
noun: "misc",
|
|
48
|
+
aliases: [],
|
|
49
|
+
kind: "configObject",
|
|
50
|
+
type: "misc",
|
|
51
|
+
description: "Miscellaneous configuration entities (free-form)",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
noun: "notifications",
|
|
55
|
+
aliases: ["notification"],
|
|
56
|
+
kind: "configObject",
|
|
57
|
+
type: "notification",
|
|
58
|
+
description: "Alert channels (email/Slack)",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
noun: "connections",
|
|
62
|
+
aliases: ["connection", "link", "links"],
|
|
63
|
+
kind: "link",
|
|
64
|
+
description: "Connections between streams/services and destinations",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
noun: "profile-builders",
|
|
68
|
+
aliases: ["profile-builder"],
|
|
69
|
+
kind: "profile-builder",
|
|
70
|
+
description: "Profile builders (identity stitching)",
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
export function verbsFor(kind) {
|
|
74
|
+
switch (kind) {
|
|
75
|
+
case "configObject":
|
|
76
|
+
return ["list", "get", "create", "update", "delete"];
|
|
77
|
+
case "workspace":
|
|
78
|
+
return ["list", "get", "create", "update", "delete"];
|
|
79
|
+
case "link":
|
|
80
|
+
return ["list", "create", "update", "delete"];
|
|
81
|
+
case "profile-builder":
|
|
82
|
+
return ["list", "create", "update", "delete"];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function findResource(name) {
|
|
86
|
+
const lower = name.toLowerCase();
|
|
87
|
+
return resources.find(r => r.noun === lower || r.aliases.includes(lower));
|
|
88
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ApiClient, ApiError } from "../lib/api-client";
|
|
2
|
+
import { authFilePath, readAuthFile, resolveAuth, updateAuthFile } from "../lib/auth-file";
|
|
3
|
+
import { red } from "../lib/chalk-code-highlight";
|
|
4
|
+
export async function setDefaultWorkspace(idOrSlug, opts) {
|
|
5
|
+
try {
|
|
6
|
+
const auth = resolveAuth(opts);
|
|
7
|
+
const client = new ApiClient(auth);
|
|
8
|
+
let workspace;
|
|
9
|
+
try {
|
|
10
|
+
workspace = await client.request({
|
|
11
|
+
method: "GET",
|
|
12
|
+
path: `/api/workspace/${encodeURIComponent(idOrSlug)}`,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
if (e instanceof ApiError && e.status === 404) {
|
|
17
|
+
throw new Error(`Workspace '${idOrSlug}' not found (or you don't have access)`);
|
|
18
|
+
}
|
|
19
|
+
throw e;
|
|
20
|
+
}
|
|
21
|
+
updateAuthFile({ defaultWorkspace: workspace.id });
|
|
22
|
+
const label = workspace.slug ? `${workspace.slug} (${workspace.id})` : workspace.id;
|
|
23
|
+
console.log(`Default workspace set to ${label}.`);
|
|
24
|
+
console.log(`Saved to ${authFilePath()}.`);
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
console.error(red(`Error: ${e instanceof Error ? e.message : String(e)}`));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function unsetDefaultWorkspace() {
|
|
32
|
+
const file = readAuthFile();
|
|
33
|
+
if (!file?.defaultWorkspace) {
|
|
34
|
+
console.log("No default workspace is set.");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
updateAuthFile({ defaultWorkspace: undefined });
|
|
38
|
+
console.log("Default workspace unset.");
|
|
39
|
+
}
|
|
@@ -3,7 +3,6 @@ import { homedir } from "os";
|
|
|
3
3
|
import inquirer from "inquirer";
|
|
4
4
|
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
5
5
|
import { loadPackageJson } from "./shared";
|
|
6
|
-
import fetch from "node-fetch";
|
|
7
6
|
import cuid from "cuid";
|
|
8
7
|
import { b, green, red } from "../lib/chalk-code-highlight";
|
|
9
8
|
import { getFunctionFromFilePath } from "../lib/compiled-function";
|
|
@@ -103,14 +102,56 @@ async function deployFunctions({ host, apikey, name: names }, projectDir, packag
|
|
|
103
102
|
}
|
|
104
103
|
profileBuilders = (await res.json()).profileBuilders;
|
|
105
104
|
}
|
|
105
|
+
const existingFunctions = await fetchExistingFunctions({ host, apikey, workspaceId: workspace.id });
|
|
106
106
|
for (const file of selectedFiles) {
|
|
107
107
|
console.log(`${b(`𝑓`)} Deploying function ${b(path.basename(file))} to workspace ${workspace.name} (${host}/${workspace.slug || workspace.id})`);
|
|
108
|
-
await deployFunction({ host, apikey }, packageJson, workspace, kind, path.resolve(functionsDir, file), profileBuilders);
|
|
108
|
+
await deployFunction(projectDir, { host, apikey }, packageJson, workspace, kind, path.resolve(functionsDir, file), profileBuilders, existingFunctions);
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
|
-
async function
|
|
111
|
+
async function fetchExistingFunctions({ host, apikey, workspaceId, }) {
|
|
112
|
+
const res = await fetch(`${host}/api/${workspaceId}/config/function`, {
|
|
113
|
+
headers: { Authorization: `Bearer ${apikey}` },
|
|
114
|
+
});
|
|
115
|
+
if (!res.ok) {
|
|
116
|
+
console.error(red(`Cannot list existing functions:\n${b(await res.text())}`));
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
const { objects } = (await res.json());
|
|
120
|
+
const bySlug = new Map();
|
|
121
|
+
const byId = new Map();
|
|
122
|
+
for (const f of objects) {
|
|
123
|
+
const entry = { id: f.id, slug: f.slug };
|
|
124
|
+
byId.set(f.id, entry);
|
|
125
|
+
if (f.slug)
|
|
126
|
+
bySlug.set(f.slug, entry);
|
|
127
|
+
}
|
|
128
|
+
return { bySlug, byId };
|
|
129
|
+
}
|
|
130
|
+
function cacheAfterCreate(cache, id, slug) {
|
|
131
|
+
const entry = { id, slug };
|
|
132
|
+
cache.byId.set(id, entry);
|
|
133
|
+
if (slug)
|
|
134
|
+
cache.bySlug.set(slug, entry);
|
|
135
|
+
}
|
|
136
|
+
function cacheAfterUpdate(cache, id, newSlug) {
|
|
137
|
+
const entry = cache.byId.get(id);
|
|
138
|
+
if (!entry) {
|
|
139
|
+
cacheAfterCreate(cache, id, newSlug);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (entry.slug && entry.slug !== newSlug) {
|
|
143
|
+
cache.bySlug.delete(entry.slug);
|
|
144
|
+
}
|
|
145
|
+
entry.slug = newSlug;
|
|
146
|
+
if (newSlug)
|
|
147
|
+
cache.bySlug.set(newSlug, entry);
|
|
148
|
+
}
|
|
149
|
+
async function deployFunction(projectDir, { host, apikey }, packageJson, workspace, kind, file, profileBuilders = [], existingFunctions = {
|
|
150
|
+
bySlug: new Map(),
|
|
151
|
+
byId: new Map(),
|
|
152
|
+
}) {
|
|
112
153
|
const code = readFileSync(file, "utf-8");
|
|
113
|
-
const wrapped = await getFunctionFromFilePath(file, kind, profileBuilders);
|
|
154
|
+
const wrapped = await getFunctionFromFilePath(projectDir, file, kind, profileBuilders);
|
|
114
155
|
const meta = wrapped.meta;
|
|
115
156
|
if (meta) {
|
|
116
157
|
console.log(` meta: slug=${meta.slug}, name=${meta.name || "not set"}`);
|
|
@@ -121,19 +162,8 @@ async function deployFunction({ host, apikey }, packageJson, workspace, kind, fi
|
|
|
121
162
|
}
|
|
122
163
|
let existingFunctionId;
|
|
123
164
|
if (meta.slug) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
Authorization: `Bearer ${apikey}`,
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
if (!res.ok) {
|
|
130
|
-
console.error(red(`Cannot add function to workspace:\n${b(await res.text())}`));
|
|
131
|
-
process.exit(1);
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
const existing = (await res.json());
|
|
135
|
-
existingFunctionId = existing.objects.find(f => f.slug === meta.slug || f.id === meta.id)?.id;
|
|
136
|
-
}
|
|
165
|
+
existingFunctionId =
|
|
166
|
+
existingFunctions.bySlug.get(meta.slug)?.id ?? (meta.id ? existingFunctions.byId.get(meta.id)?.id : undefined);
|
|
137
167
|
}
|
|
138
168
|
let functionPayload = {};
|
|
139
169
|
if (kind === "profile") {
|
|
@@ -172,6 +202,7 @@ async function deployFunction({ host, apikey }, packageJson, workspace, kind, fi
|
|
|
172
202
|
process.exit(1);
|
|
173
203
|
}
|
|
174
204
|
else {
|
|
205
|
+
cacheAfterCreate(existingFunctions, id, meta.slug);
|
|
175
206
|
console.log(`Function ${b(meta.name)} was successfully added to workspace ${workspace.name} with id: ${b(id)}`);
|
|
176
207
|
}
|
|
177
208
|
}
|
|
@@ -199,6 +230,7 @@ async function deployFunction({ host, apikey }, packageJson, workspace, kind, fi
|
|
|
199
230
|
process.exit(1);
|
|
200
231
|
}
|
|
201
232
|
else {
|
|
233
|
+
cacheAfterUpdate(existingFunctions, id, meta.slug);
|
|
202
234
|
console.log(`${green(`✓`)} ${b(meta.name)} deployed successfully!`);
|
|
203
235
|
}
|
|
204
236
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { red } from "../lib/chalk-code-highlight";
|
|
3
|
+
import { normalizeHost, readAuthFile, resolveAuth } from "../lib/auth-file";
|
|
4
|
+
import { fetchSpec } from "../lib/spec";
|
|
5
|
+
import { DEFAULT_OUTPUT, SUPPORTED_OUTPUTS, print } from "../lib/renderer";
|
|
6
|
+
export function buildSpecCommand() {
|
|
7
|
+
return new Command("spec")
|
|
8
|
+
.description("Print the live OpenAPI spec served at /api/spec")
|
|
9
|
+
.option("-o, --output <format>", `Output format: ${SUPPORTED_OUTPUTS.join(", ")}`, DEFAULT_OUTPUT)
|
|
10
|
+
.option("-h, --host <host>", "Jitsu host (overrides ~/.jitsu/jitsu-cli.json)")
|
|
11
|
+
.option("-k, --apikey <api-key>", "API key (overrides ~/.jitsu/jitsu-cli.json)")
|
|
12
|
+
.action(async (opts) => {
|
|
13
|
+
try {
|
|
14
|
+
const auth = (() => {
|
|
15
|
+
try {
|
|
16
|
+
return resolveAuth(opts);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
const host = opts.host ?? readAuthFile()?.host ?? "https://use.jitsu.com";
|
|
20
|
+
return { host: normalizeHost(host), apikey: "anonymous" };
|
|
21
|
+
}
|
|
22
|
+
})();
|
|
23
|
+
const spec = await fetchSpec(auth);
|
|
24
|
+
print(spec, opts.output);
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
console.error(red(`Error: ${e instanceof Error ? e.message : String(e)}`));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
package/compiled/src/index.js
CHANGED
|
@@ -7,9 +7,17 @@ import { build } from "./commands/build";
|
|
|
7
7
|
import { test } from "./commands/test";
|
|
8
8
|
import { jitsuCliVersion, jitsuCliPackageName } from "./lib/version";
|
|
9
9
|
import { whoami } from "./commands/whoami";
|
|
10
|
-
|
|
10
|
+
import { buildConfigCommand } from "./commands/config";
|
|
11
|
+
import { buildSpecCommand } from "./commands/spec";
|
|
12
|
+
import { setDefaultWorkspace, unsetDefaultWorkspace } from "./commands/default-workspace";
|
|
13
|
+
import { preprocessArgv } from "./lib/body-fields";
|
|
14
|
+
process.argv = preprocessArgv(process.argv);
|
|
15
|
+
const isPipedStdout = !process.stdout.isTTY;
|
|
16
|
+
if (!isPipedStdout) {
|
|
17
|
+
process.stderr.write(figlet.textSync("Jitsu CLI", { horizontalLayout: "full" }) + "\n");
|
|
18
|
+
}
|
|
11
19
|
const p = new Command();
|
|
12
|
-
p.name(jitsuCliPackageName).description("CLI
|
|
20
|
+
p.name(jitsuCliPackageName).description("Jitsu CLI — manage workspaces, configuration objects, and extensions");
|
|
13
21
|
p.command("init")
|
|
14
22
|
.description("Initialize a new Jitsu extension project")
|
|
15
23
|
.arguments("[dir]")
|
|
@@ -45,6 +53,17 @@ p.command("deploy")
|
|
|
45
53
|
.option("-t, --type <type>", "entity type to deploy", "function")
|
|
46
54
|
.option("-n, --name <name...>", "limit deploy to provided entities only. (Optional)")
|
|
47
55
|
.action(deploy);
|
|
56
|
+
p.command("set-default-workspace")
|
|
57
|
+
.description("Save a default workspace to ~/.jitsu/jitsu-cli.json. Subsequent `jitsu config` commands use it when -w is omitted.")
|
|
58
|
+
.argument("<id-or-slug>", "Workspace id or slug")
|
|
59
|
+
.option("-h, --host <host>", "Jitsu host (overrides ~/.jitsu/jitsu-cli.json)")
|
|
60
|
+
.option("-k, --apikey <api-key>", "API key in form keyId:secret (overrides ~/.jitsu/jitsu-cli.json)")
|
|
61
|
+
.action(setDefaultWorkspace);
|
|
62
|
+
p.command("unset-default-workspace")
|
|
63
|
+
.description("Remove the saved default workspace from ~/.jitsu/jitsu-cli.json")
|
|
64
|
+
.action(unsetDefaultWorkspace);
|
|
65
|
+
p.addCommand(buildConfigCommand());
|
|
66
|
+
p.addCommand(buildSpecCommand());
|
|
48
67
|
p.version(jitsuCliPackageName + " " + jitsuCliVersion, "-v, --version");
|
|
49
68
|
p.helpOption("--help", "display help for command");
|
|
50
69
|
p.parse();
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export class ApiError extends Error {
|
|
2
|
+
constructor(status, message, payload) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.status = status;
|
|
5
|
+
this.payload = payload;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export class ApiClient {
|
|
9
|
+
constructor(auth) {
|
|
10
|
+
this.auth = auth;
|
|
11
|
+
}
|
|
12
|
+
url(path, query) {
|
|
13
|
+
const base = this.auth.host;
|
|
14
|
+
const p = path.startsWith("/") ? path : `/${path}`;
|
|
15
|
+
if (!query)
|
|
16
|
+
return `${base}${p}`;
|
|
17
|
+
const qs = Object.entries(query)
|
|
18
|
+
.filter(([, v]) => v !== undefined && v !== null && v !== "")
|
|
19
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
|
|
20
|
+
.join("&");
|
|
21
|
+
return qs ? `${base}${p}?${qs}` : `${base}${p}`;
|
|
22
|
+
}
|
|
23
|
+
async request(req) {
|
|
24
|
+
const method = req.method ?? "GET";
|
|
25
|
+
const headers = {
|
|
26
|
+
Accept: "application/json",
|
|
27
|
+
Authorization: `Bearer ${this.auth.apikey}`,
|
|
28
|
+
};
|
|
29
|
+
let body;
|
|
30
|
+
if (req.body !== undefined) {
|
|
31
|
+
headers["Content-Type"] = "application/json";
|
|
32
|
+
body = JSON.stringify(req.body);
|
|
33
|
+
}
|
|
34
|
+
const res = await fetch(this.url(req.path, req.query), { method, headers, body });
|
|
35
|
+
const text = await res.text();
|
|
36
|
+
let parsed = text;
|
|
37
|
+
if (text && res.headers.get("content-type")?.includes("application/json")) {
|
|
38
|
+
try {
|
|
39
|
+
parsed = JSON.parse(text);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
const msg = (parsed && typeof parsed === "object" && "message" in parsed
|
|
46
|
+
? String(parsed.message)
|
|
47
|
+
: undefined) ?? `HTTP ${res.status} ${method} ${req.path}`;
|
|
48
|
+
throw new ApiError(res.status, msg, parsed);
|
|
49
|
+
}
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
const DEFAULT_HOST = "https://use.jitsu.com";
|
|
5
|
+
export function authFilePath() {
|
|
6
|
+
return `${homedir()}/.jitsu/jitsu-cli.json`;
|
|
7
|
+
}
|
|
8
|
+
export function readAuthFile() {
|
|
9
|
+
const path = authFilePath();
|
|
10
|
+
if (!fs.existsSync(path))
|
|
11
|
+
return undefined;
|
|
12
|
+
return JSON.parse(readFileSync(path, { encoding: "utf-8" }));
|
|
13
|
+
}
|
|
14
|
+
export function updateAuthFile(patch) {
|
|
15
|
+
const path = authFilePath();
|
|
16
|
+
const existing = readAuthFile() ?? {};
|
|
17
|
+
const next = { ...existing };
|
|
18
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
19
|
+
if (v === undefined)
|
|
20
|
+
delete next[k];
|
|
21
|
+
else
|
|
22
|
+
next[k] = v;
|
|
23
|
+
}
|
|
24
|
+
mkdirSync(`${homedir()}/.jitsu`, { recursive: true });
|
|
25
|
+
writeFileSync(path, JSON.stringify(next, null, 2));
|
|
26
|
+
return next;
|
|
27
|
+
}
|
|
28
|
+
export function readDefaultWorkspace() {
|
|
29
|
+
return readAuthFile()?.defaultWorkspace;
|
|
30
|
+
}
|
|
31
|
+
export function resolveAuth(opts) {
|
|
32
|
+
let host = opts.host;
|
|
33
|
+
let apikey = opts.apikey;
|
|
34
|
+
if (!host || !apikey) {
|
|
35
|
+
const file = readAuthFile();
|
|
36
|
+
if (file) {
|
|
37
|
+
if (!host)
|
|
38
|
+
host = file.host;
|
|
39
|
+
if (!apikey)
|
|
40
|
+
apikey = file.apikey;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!host)
|
|
44
|
+
host = process.env.JITSU_HOST;
|
|
45
|
+
if (!apikey)
|
|
46
|
+
apikey = process.env.JITSU_APIKEY;
|
|
47
|
+
if (!host)
|
|
48
|
+
host = DEFAULT_HOST;
|
|
49
|
+
if (!apikey) {
|
|
50
|
+
throw new Error("Not authenticated. Run `jitsu login`, set JITSU_APIKEY, or pass --apikey <key>.");
|
|
51
|
+
}
|
|
52
|
+
return { host: normalizeHost(host), apikey };
|
|
53
|
+
}
|
|
54
|
+
export function normalizeHost(host) {
|
|
55
|
+
let url = host;
|
|
56
|
+
if (!url.startsWith("http")) {
|
|
57
|
+
if (url.startsWith("localhost") || /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/.test(url)) {
|
|
58
|
+
url = "http://" + url;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
url = "https://" + url;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (url.endsWith("/"))
|
|
65
|
+
url = url.slice(0, -1);
|
|
66
|
+
return url;
|
|
67
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import yaml from "js-yaml";
|
|
3
|
+
import { parseScalar, setDottedPath } from "./dotted";
|
|
4
|
+
export function buildBody(sources) {
|
|
5
|
+
const layers = [];
|
|
6
|
+
if (sources.file)
|
|
7
|
+
layers.push(loadFile(sources.file));
|
|
8
|
+
if (sources.json)
|
|
9
|
+
layers.push(parseInlineJson(sources.json));
|
|
10
|
+
if (sources.fields && Object.keys(sources.fields).length > 0) {
|
|
11
|
+
const obj = {};
|
|
12
|
+
for (const [path, raw] of Object.entries(sources.fields)) {
|
|
13
|
+
setDottedPath(obj, path, parseScalar(raw));
|
|
14
|
+
}
|
|
15
|
+
layers.push(obj);
|
|
16
|
+
}
|
|
17
|
+
if (layers.length === 0)
|
|
18
|
+
return undefined;
|
|
19
|
+
return layers.reduce((acc, layer) => deepMerge(acc, layer), {});
|
|
20
|
+
}
|
|
21
|
+
function loadFile(path) {
|
|
22
|
+
const text = path === "-" ? fs.readFileSync(0, "utf-8") : fs.readFileSync(path, "utf-8");
|
|
23
|
+
const parsed = yaml.load(text);
|
|
24
|
+
if (parsed === null || parsed === undefined) {
|
|
25
|
+
throw new Error(`File ${path} parsed as empty`);
|
|
26
|
+
}
|
|
27
|
+
return parsed;
|
|
28
|
+
}
|
|
29
|
+
function parseInlineJson(s) {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(s);
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
throw new Error(`--json value is not valid JSON: ${e.message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function isPlainObject(v) {
|
|
38
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
39
|
+
}
|
|
40
|
+
function deepMerge(target, source) {
|
|
41
|
+
if (!isPlainObject(target) || !isPlainObject(source))
|
|
42
|
+
return source;
|
|
43
|
+
const out = { ...target };
|
|
44
|
+
for (const [k, v] of Object.entries(source)) {
|
|
45
|
+
if (isPlainObject(v) && isPlainObject(out[k])) {
|
|
46
|
+
out[k] = deepMerge(out[k], v);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
out[k] = v;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const RESERVED = new Set([
|
|
2
|
+
"workspace",
|
|
3
|
+
"output",
|
|
4
|
+
"host",
|
|
5
|
+
"apikey",
|
|
6
|
+
"file",
|
|
7
|
+
"json",
|
|
8
|
+
"cascade",
|
|
9
|
+
"strict",
|
|
10
|
+
"from",
|
|
11
|
+
"to",
|
|
12
|
+
"help",
|
|
13
|
+
"version",
|
|
14
|
+
]);
|
|
15
|
+
const fields = {};
|
|
16
|
+
function shouldExtract(argv) {
|
|
17
|
+
for (let i = 2; i < argv.length; i++) {
|
|
18
|
+
const a = argv[i];
|
|
19
|
+
if (a.startsWith("-"))
|
|
20
|
+
continue;
|
|
21
|
+
return a === "config";
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
export function preprocessArgv(argv) {
|
|
26
|
+
if (!shouldExtract(argv))
|
|
27
|
+
return argv;
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const arg of argv) {
|
|
30
|
+
const m = /^--([a-zA-Z][\w.-]*)=([\s\S]*)$/.exec(arg);
|
|
31
|
+
if (m) {
|
|
32
|
+
const head = m[1].split(".")[0];
|
|
33
|
+
if (!RESERVED.has(head)) {
|
|
34
|
+
fields[m[1]] = m[2];
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
out.push(arg);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
export function consumeBodyFields() {
|
|
43
|
+
const copy = { ...fields };
|
|
44
|
+
for (const k of Object.keys(fields))
|
|
45
|
+
delete fields[k];
|
|
46
|
+
return copy;
|
|
47
|
+
}
|
|
@@ -1,33 +1,35 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
|
-
import
|
|
2
|
+
import * as esbuild from "esbuild";
|
|
3
3
|
import { assertDefined, assertTrue } from "juava";
|
|
4
4
|
function getSlug(filePath) {
|
|
5
|
-
return filePath.split("/").pop()?.replace(".ts", "");
|
|
5
|
+
return filePath.split("/").pop()?.replace(".ts", "").replace(".js", "");
|
|
6
6
|
}
|
|
7
|
-
export async function getFunctionFromFilePath(filePath, kind, profileBuilders = []) {
|
|
7
|
+
export async function getFunctionFromFilePath(projectDir, filePath, kind, profileBuilders = []) {
|
|
8
8
|
if (!fs.existsSync(filePath)) {
|
|
9
9
|
throw new Error(`Cannot load function from file ${filePath}: file doesn't exist`);
|
|
10
10
|
}
|
|
11
11
|
else if (!fs.statSync(filePath).isFile()) {
|
|
12
12
|
throw new Error(`Cannot load function from file ${filePath}: path is not a file`);
|
|
13
13
|
}
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
const result = await esbuild.transform(fs.readFileSync(filePath, "utf-8"), {
|
|
15
|
+
loader: filePath.endsWith(".ts") ? "ts" : "js",
|
|
16
|
+
format: "cjs",
|
|
17
|
+
platform: "node",
|
|
18
18
|
});
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
const code = result.code;
|
|
20
|
+
const module = { exports: {} };
|
|
21
|
+
const exports = module.exports;
|
|
22
|
+
const require = (id) => {
|
|
23
|
+
return {};
|
|
24
|
+
};
|
|
25
|
+
eval(code);
|
|
26
|
+
const moduleExports = module.exports;
|
|
27
|
+
assertDefined(moduleExports.default, `Function from ${filePath} doesn't have default export. Exported symbols: ${Object.keys(moduleExports)}`);
|
|
28
|
+
assertTrue(typeof moduleExports.default === "function", `Default export from ${filePath} is not a function`);
|
|
29
|
+
let name = moduleExports.config?.name || moduleExports.config?.slug || getSlug(filePath);
|
|
30
|
+
let id = moduleExports.config?.id;
|
|
29
31
|
if (kind === "profile") {
|
|
30
|
-
const profileBuilderId =
|
|
32
|
+
const profileBuilderId = moduleExports.config?.profileBuilderId;
|
|
31
33
|
const profileBuilder = profileBuilders.find(pb => pb.id === profileBuilderId);
|
|
32
34
|
if (!profileBuilder) {
|
|
33
35
|
throw new Error(`Cannot find profile builder with id ${profileBuilderId} for profile function ${filePath}. Please setup Profile Builder in UI first.`);
|
|
@@ -39,12 +41,12 @@ export async function getFunctionFromFilePath(filePath, kind, profileBuilders =
|
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
return {
|
|
42
|
-
func:
|
|
44
|
+
func: moduleExports.default,
|
|
43
45
|
meta: {
|
|
44
|
-
slug:
|
|
46
|
+
slug: moduleExports.config?.slug || getSlug(filePath),
|
|
45
47
|
id: id,
|
|
46
48
|
name: name,
|
|
47
|
-
description:
|
|
49
|
+
description: moduleExports.config?.description,
|
|
48
50
|
},
|
|
49
51
|
};
|
|
50
52
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function parseScalar(raw) {
|
|
2
|
+
if (raw === "")
|
|
3
|
+
return "";
|
|
4
|
+
const trimmed = raw.trim();
|
|
5
|
+
const first = trimmed[0];
|
|
6
|
+
const looksLikeJson = first === "{" ||
|
|
7
|
+
first === "[" ||
|
|
8
|
+
first === '"' ||
|
|
9
|
+
trimmed === "true" ||
|
|
10
|
+
trimmed === "false" ||
|
|
11
|
+
trimmed === "null" ||
|
|
12
|
+
/^-?\d/.test(trimmed);
|
|
13
|
+
if (looksLikeJson) {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(trimmed);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return raw;
|
|
21
|
+
}
|
|
22
|
+
export function setDottedPath(target, path, value) {
|
|
23
|
+
const parts = path.split(".");
|
|
24
|
+
let node = target;
|
|
25
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
26
|
+
const key = parts[i];
|
|
27
|
+
if (node[key] == null || typeof node[key] !== "object" || Array.isArray(node[key])) {
|
|
28
|
+
node[key] = {};
|
|
29
|
+
}
|
|
30
|
+
node = node[key];
|
|
31
|
+
}
|
|
32
|
+
node[parts[parts.length - 1]] = value;
|
|
33
|
+
return target;
|
|
34
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import yaml from "js-yaml";
|
|
2
|
+
const renderers = {
|
|
3
|
+
yaml: {
|
|
4
|
+
format: "yaml",
|
|
5
|
+
render(value) {
|
|
6
|
+
if (value === undefined)
|
|
7
|
+
return "";
|
|
8
|
+
return yaml.dump(value, { skipInvalid: true, noRefs: true, sortKeys: false, lineWidth: 120 });
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
json: {
|
|
12
|
+
format: "json",
|
|
13
|
+
render(value) {
|
|
14
|
+
return JSON.stringify(value, null, 2) + "\n";
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
export const SUPPORTED_OUTPUTS = Object.keys(renderers);
|
|
19
|
+
export const DEFAULT_OUTPUT = "yaml";
|
|
20
|
+
export function getRenderer(format) {
|
|
21
|
+
const key = (format ?? DEFAULT_OUTPUT).toLowerCase();
|
|
22
|
+
const r = renderers[key];
|
|
23
|
+
if (!r) {
|
|
24
|
+
throw new Error(`Unsupported output format '${format}'. Supported: ${SUPPORTED_OUTPUTS.join(", ")}`);
|
|
25
|
+
}
|
|
26
|
+
return r;
|
|
27
|
+
}
|
|
28
|
+
export function registerRenderer(r) {
|
|
29
|
+
renderers[r.format.toLowerCase()] = r;
|
|
30
|
+
}
|
|
31
|
+
export function print(value, format) {
|
|
32
|
+
process.stdout.write(getRenderer(format).render(value));
|
|
33
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ApiClient } from "./api-client";
|
|
2
|
+
export async function fetchSpec(auth) {
|
|
3
|
+
const client = new ApiClient(auth);
|
|
4
|
+
return client.request({ method: "GET", path: "/api/spec" });
|
|
5
|
+
}
|
|
6
|
+
export function findOperation(spec, path, method) {
|
|
7
|
+
const item = spec.paths?.[path];
|
|
8
|
+
if (!item)
|
|
9
|
+
return undefined;
|
|
10
|
+
return item[method.toLowerCase()];
|
|
11
|
+
}
|