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.
Files changed (41) hide show
  1. package/bin/jitsu +3 -0
  2. package/build.mts +39 -0
  3. package/compiled/package.json +16 -22
  4. package/compiled/src/commands/build.js +16 -24
  5. package/compiled/src/commands/config/handlers.js +292 -0
  6. package/compiled/src/commands/config/index.js +148 -0
  7. package/compiled/src/commands/config/resources.js +88 -0
  8. package/compiled/src/commands/default-workspace.js +39 -0
  9. package/compiled/src/commands/deploy.js +49 -17
  10. package/compiled/src/commands/spec.js +31 -0
  11. package/compiled/src/index.js +21 -2
  12. package/compiled/src/lib/api-client.js +52 -0
  13. package/compiled/src/lib/auth-file.js +67 -0
  14. package/compiled/src/lib/body-builder.js +53 -0
  15. package/compiled/src/lib/body-fields.js +47 -0
  16. package/compiled/src/lib/compiled-function.js +23 -21
  17. package/compiled/src/lib/dotted.js +34 -0
  18. package/compiled/src/lib/renderer.js +33 -0
  19. package/compiled/src/lib/spec.js +11 -0
  20. package/dist/main.js +46393 -92868
  21. package/dist/main.js.map +7 -1
  22. package/package.json +16 -22
  23. package/src/commands/build.ts +21 -26
  24. package/src/commands/config/handlers.ts +339 -0
  25. package/src/commands/config/index.ts +171 -0
  26. package/src/commands/config/resources.ts +110 -0
  27. package/src/commands/default-workspace.ts +44 -0
  28. package/src/commands/deploy.ts +90 -16
  29. package/src/commands/spec.ts +32 -0
  30. package/src/index.ts +34 -17
  31. package/src/lib/api-client.ts +64 -0
  32. package/src/lib/auth-file.ts +83 -0
  33. package/src/lib/body-builder.ts +68 -0
  34. package/src/lib/body-fields.ts +61 -0
  35. package/src/lib/compiled-function.ts +30 -23
  36. package/src/lib/dotted.ts +43 -0
  37. package/src/lib/renderer.ts +44 -0
  38. package/src/lib/spec.ts +32 -0
  39. package/dist/140.js +0 -452
  40. package/dist/140.js.map +0 -1
  41. 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 deployFunction({ host, apikey }, packageJson, workspace, kind, file, profileBuilders = []) {
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
- const res = await fetch(`${host}/api/${workspace.id}/config/function`, {
125
- headers: {
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
+ }
@@ -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
- console.log(figlet.textSync("Jitsu CLI", { horizontalLayout: "full" }));
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 command to create, test and deploy extensions for Jitsu Next");
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 { rollup } from "rollup";
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 bundle = await rollup({
15
- input: [filePath],
16
- external: ["@jitsu/functions-lib"],
17
- logLevel: "silent",
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 output = await bundle.generate({
20
- file: filePath,
21
- format: "commonjs",
22
- });
23
- const exports = {};
24
- eval(output.output[0].code);
25
- assertDefined(exports.default, `Function from ${filePath} doesn't have default export. Exported symbols: ${Object.keys(exports)}`);
26
- assertTrue(typeof exports.default === "function", `Default export from ${filePath} is not a function`);
27
- let name = exports.config?.name || exports.config?.slug || getSlug(filePath);
28
- let id = exports.config?.id;
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 = exports.config?.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: exports.default,
44
+ func: moduleExports.default,
43
45
  meta: {
44
- slug: exports.config?.slug || getSlug(filePath),
46
+ slug: moduleExports.config?.slug || getSlug(filePath),
45
47
  id: id,
46
48
  name: name,
47
- description: exports.config?.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
+ }