state.svelte.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. import { Channel, invoke } from "@tauri-apps/api/core";
  2. import type {
  3. Workspace,
  4. WorkspaceEntryBase,
  5. RequestBody,
  6. WorkspaceEntry,
  7. WorkspaceEnvironment,
  8. EnvVariable,
  9. RequestUrl,
  10. RequestHeader,
  11. RequestPathParam,
  12. Authentication,
  13. AuthType,
  14. HttpResponse,
  15. ResponseResult,
  16. } from "./types";
  17. import { getSetting, setSetting } from "./settings.svelte";
  18. export type WorkspaceState = {
  19. /**
  20. * Currently selected workspace.
  21. */
  22. workspace: Workspace | null;
  23. /**
  24. * Currently selected workspace entry.
  25. */
  26. entry: WorkspaceEntry | null;
  27. /**
  28. * Workspace root entries.
  29. */
  30. roots: number[];
  31. /**
  32. * Workspace entry root => children mappings.
  33. */
  34. children: Record<number, number[]>;
  35. /**
  36. * All workspace entries.
  37. */
  38. indexes: Record<number, WorkspaceEntryBase>;
  39. /**
  40. * Currently selected workspace environments.
  41. */
  42. environments: WorkspaceEnvironment[];
  43. /**
  44. * Currently selected environment.
  45. */
  46. environment: WorkspaceEnvironment | null;
  47. /**
  48. * All workspace authentication schemes.
  49. */
  50. auth: Authentication[];
  51. /**
  52. * Set of pending sent requests.
  53. */
  54. pendingRequests: number[];
  55. /**
  56. * Maps request IDs to their latest response.
  57. */
  58. responses: Record<number, HttpResponse>;
  59. };
  60. export const state: WorkspaceState = $state({
  61. workspace: null,
  62. entry: null,
  63. roots: [],
  64. children: {},
  65. indexes: {},
  66. environments: [],
  67. environment: null,
  68. auth: [],
  69. pendingRequests: [],
  70. responses: {},
  71. });
  72. const index = (entry: WorkspaceEntry) => {
  73. console.log("indexing", entry);
  74. state.indexes[entry.id] = entry;
  75. if (entry.parent_id != null) {
  76. if (state.children[entry.parent_id]) {
  77. state.children[entry.parent_id].push(entry.id);
  78. } else {
  79. state.children[entry.parent_id] = [entry.id];
  80. }
  81. } else {
  82. state.roots.push(entry.id);
  83. }
  84. };
  85. function reset() {
  86. state.children = {};
  87. state.indexes = {};
  88. state.roots = [];
  89. state.entry = null;
  90. state.environment = null;
  91. state.environments = [];
  92. state.auth = [];
  93. state.pendingRequests = [];
  94. state.responses = {};
  95. }
  96. export async function selectEnvironment(
  97. id: number | null,
  98. save: boolean = true,
  99. ) {
  100. if (id === null) {
  101. state.environment = null;
  102. let env = await getSetting("lastEnvironment");
  103. if (env) {
  104. env[state.workspace!!.id] = null;
  105. } else {
  106. env = { [state.workspace!!.id]: null };
  107. }
  108. setSetting("lastEnvironment", env);
  109. return;
  110. }
  111. state.environment = state.environments.find((e) => e.id === id) ?? null;
  112. console.debug("selected environment:", state.environment?.name);
  113. if (!save) {
  114. return;
  115. }
  116. let env = await getSetting("lastEnvironment");
  117. if (env) {
  118. env[state.workspace!!.id] = id;
  119. } else {
  120. env = { [state.workspace!!.id]: id };
  121. }
  122. setSetting("lastEnvironment", env);
  123. }
  124. export function selectWorkspace(ws: Workspace) {
  125. console.debug("selecting workspace:", ws.name);
  126. state.workspace = ws;
  127. }
  128. export async function selectEntry(id: number) {
  129. const entry = await invoke<WorkspaceEntryResponse>("get_workspace_entry", {
  130. entryId: id,
  131. });
  132. switch (entry.type) {
  133. case "Collection": {
  134. state.entry = entry.data;
  135. break;
  136. }
  137. case "Request": {
  138. state.entry = {
  139. ...entry.data.entry,
  140. method: entry.data.method,
  141. url: entry.data.url,
  142. headers: entry.data.headers,
  143. body: entry.data.body,
  144. path: entry.data.path_params,
  145. };
  146. break;
  147. }
  148. }
  149. console.log("selected entry:", $state.snapshot(state.entry));
  150. if (state.entry.parent_id != null) {
  151. let parent = state.indexes[state.entry.parent_id];
  152. while (parent) {
  153. parent.open = true;
  154. if (parent.parent_id === null) {
  155. break;
  156. }
  157. parent = state.indexes[parent.parent_id];
  158. }
  159. }
  160. if (state.entry.type === "Request") {
  161. parseUrl(state.entry!!.url)
  162. .then(() =>
  163. console.debug("working URL:", $state.snapshot(state.entry.workingUrl)),
  164. )
  165. .catch((e) => {
  166. console.error("error parsing URL", e);
  167. });
  168. expandUrl()
  169. .then(() =>
  170. console.debug(
  171. "expanded URL:",
  172. $state.snapshot(state.entry.expandedUrl),
  173. ),
  174. )
  175. .catch((e) => {
  176. console.error("error expanding URL", e);
  177. });
  178. }
  179. }
  180. // COMMANDS
  181. export async function createWorkspace(name: string): Promise<Workspace> {
  182. return invoke<Workspace>("create_workspace", { name });
  183. }
  184. export async function listWorkspaces(): Promise<Workspace[]> {
  185. return invoke<Workspace[]>("list_workspaces");
  186. }
  187. export async function loadWorkspace(ws: Workspace) {
  188. reset();
  189. state.workspace = ws;
  190. const entries = await invoke<WorkspaceEntryBase[]>("list_workspace_entries", {
  191. id: state.workspace.id,
  192. });
  193. for (const entry of entries) {
  194. // if (entry.type === "Request") {
  195. // } else {
  196. index(entry);
  197. // }
  198. }
  199. await loadEnvironments(state.workspace.id);
  200. await loadAuths(state.workspace.id);
  201. }
  202. export function createRequest(parentId?: number) {
  203. if (state.workspace == null) {
  204. console.warn("create request called with no active workspace");
  205. return;
  206. }
  207. const data = {
  208. Request: {
  209. name: "",
  210. workspace_id: state.workspace.id,
  211. parent_id: parentId,
  212. method: "GET",
  213. url: "",
  214. auth_inherit: parentId !== undefined,
  215. },
  216. };
  217. invoke<WorkspaceEntryBase>("create_workspace_entry", {
  218. data,
  219. }).then((entry) => {
  220. index(entry);
  221. selectEntry(entry.id);
  222. console.log("request created:", entry);
  223. });
  224. }
  225. export function createCollection(parentId?: number) {
  226. if (state.workspace == null) {
  227. console.warn("create collection called with no active workspace");
  228. return;
  229. }
  230. const data = {
  231. Collection: {
  232. name: "",
  233. workspace_id: state.workspace.id,
  234. parent_id: parentId,
  235. auth_inherit: parentId !== undefined,
  236. },
  237. };
  238. invoke<WorkspaceEntryBase>("create_workspace_entry", {
  239. data,
  240. }).then((entry) => {
  241. index(entry);
  242. selectEntry(entry.id);
  243. console.log("collection created:", entry);
  244. });
  245. }
  246. export async function loadEnvironments(workspaceId: number) {
  247. state.environments = await invoke<WorkspaceEnvironment[]>(
  248. "list_environments",
  249. { workspaceId },
  250. );
  251. const lastEnv = await getSetting("lastEnvironment");
  252. if (lastEnv && lastEnv[workspaceId] !== undefined) {
  253. selectEnvironment(lastEnv[workspaceId], false);
  254. }
  255. }
  256. export async function loadAuths(workspaceId: number) {
  257. state.auth = await invoke<Authentication[]>("list_auth", { workspaceId });
  258. }
  259. export async function createEnvironment(workspaceId: number, name: string) {
  260. console.debug("creating environment in", workspaceId);
  261. const env = await invoke<WorkspaceEnvironment>("create_env", {
  262. workspaceId,
  263. name,
  264. });
  265. state.environment = env;
  266. state.environments.push(state.environment);
  267. }
  268. export async function updateEnvironment() {
  269. if (!state.environment) {
  270. console.warn("attempted to persist null env");
  271. return;
  272. }
  273. console.debug("updating environment", state.environment);
  274. await invoke("update_env", {
  275. id: state.environment.id,
  276. name: state.environment.name,
  277. });
  278. }
  279. export async function sendRequest(): Promise<void> {
  280. const reqId = state.entry!!.id;
  281. if (state.pendingRequests.includes(reqId)) {
  282. console.warn("request is already pending", reqId);
  283. }
  284. console.log("sending request", reqId);
  285. const onComplete = new Channel<ResponseResult>();
  286. console.time("request-" + reqId);
  287. onComplete.onmessage = (response) => {
  288. console.log("received response", response);
  289. switch (response.type) {
  290. case "Ok": {
  291. state.responses[state.entry!!.id] = response.data;
  292. console.log(state.responses);
  293. break;
  294. }
  295. case "Err": {
  296. console.error("received response error", response.data);
  297. break;
  298. }
  299. default: {
  300. console.error("unrecognized response type", response.type);
  301. break;
  302. }
  303. }
  304. console.timeEnd("request-" + reqId);
  305. state.pendingRequests = state.pendingRequests.filter((id) => id !== reqId);
  306. };
  307. await invoke<HttpResponse>("send_request", {
  308. reqId,
  309. envId: state.environment?.id,
  310. onComplete,
  311. });
  312. state.pendingRequests.push(reqId);
  313. }
  314. export async function cancelRequest(): Promise<void> {
  315. if (!state.pendingRequests.includes(state.entry!!.id)) {
  316. console.warn("nothing to cancel!");
  317. return;
  318. }
  319. console.log("cancelling request");
  320. await invoke("cancel_request", { reqId: state.entry!!.id });
  321. console.timeEnd("request-" + state.entry!!.id);
  322. state.pendingRequests = state.pendingRequests.filter(
  323. (id) => id !== state.entry!!.id,
  324. );
  325. }
  326. export async function updateEntryName(name: string) {
  327. if (!state.entry) {
  328. console.warn("attempted to persist null entry");
  329. return;
  330. }
  331. console.debug(state.entry.id, "updating entry name to", name);
  332. const data =
  333. state.entry.type === "Request"
  334. ? {
  335. Request: {
  336. base: {
  337. name,
  338. },
  339. },
  340. }
  341. : { Collection: { name } };
  342. await invoke("update_workspace_entry", {
  343. entryId: state.entry.id,
  344. data,
  345. });
  346. state.indexes[state.entry.id].name = name;
  347. }
  348. export async function parseUrl(url: string) {
  349. console.debug("parsing", $state.snapshot(url));
  350. state.entry!!.workingUrl = await invoke<RequestUrl>("parse_url", {
  351. url,
  352. envId: state.environment?.id,
  353. });
  354. }
  355. /**
  356. * Update a request's URL string. If `usePathparams` is true, path entries
  357. * from state.entry.path will be used to replace those at the same position and should
  358. * be set to true whenever this is called from an input field of a destructured URL.
  359. */
  360. export async function updateUrl(u: string, usePathParams: boolean) {
  361. console.log(u, usePathParams);
  362. const [url, params] = await invoke<any[]>("update_url", {
  363. entryId: state.entry!!.id,
  364. usePathParams,
  365. url: u,
  366. pathParams: state.entry.path,
  367. });
  368. console.log(url);
  369. state.entry!!.url = u;
  370. state.entry.path = params;
  371. state.entry.workingUrl = url;
  372. expandUrl();
  373. console.debug("updated", $state.snapshot(state.entry));
  374. }
  375. export async function expandUrl() {
  376. state.entry!!.expandedUrl = await invoke<string>("expand_url", {
  377. entryId: state.entry!!.id,
  378. envId: state.environment?.id,
  379. url: state.entry!!.url,
  380. });
  381. }
  382. export async function insertEnvVariable(
  383. workspaceId: number,
  384. envId: number,
  385. name: string = "",
  386. value: string = "",
  387. secret: boolean = false,
  388. ) {
  389. const v = await invoke<EnvVariable>("insert_env_var", {
  390. workspaceId,
  391. envId,
  392. name,
  393. value,
  394. secret,
  395. });
  396. state.environment?.variables.push(v);
  397. }
  398. export async function updateEnvVariable(v: EnvVariable) {
  399. if (v.name.length === 0 && v.value.length === 0) {
  400. console.debug("deleting var:", v);
  401. return deleteEnvVariable(v.id);
  402. }
  403. console.debug("updating var:", v);
  404. return invoke("update_env_var", {
  405. id: v.id,
  406. name: v.name,
  407. value: v.value,
  408. secret: v.secret,
  409. });
  410. }
  411. export async function deleteEnvVariable(id: number) {
  412. await invoke("delete_env_var", { id });
  413. state.environment!!.variables = state.environment!!.variables.filter(
  414. (v) => v.id !== id,
  415. );
  416. }
  417. export async function insertHeader() {
  418. const header = await invoke("insert_header", {
  419. entryId: state.entry!!.id,
  420. insert: { name: "", value: "" },
  421. });
  422. state.entry!!.headers.push(header);
  423. }
  424. export async function updateHeader(id: number, name: string, value: string) {
  425. await invoke("update_header", {
  426. update: { id, name, value },
  427. });
  428. const header = state.entry!!.headers.find((header) => header.id === id);
  429. header.name = name;
  430. header.value = value;
  431. }
  432. export async function deleteHeader(id: number) {
  433. await invoke("delete_header", {
  434. headerId: id,
  435. });
  436. state.entry!!.headers = state.entry!!.headers.filter(
  437. (header) => header.id !== id,
  438. );
  439. }
  440. export async function deleteBody() {
  441. if (state.entry!!.body === null) {
  442. console.warn("attempted to delete null body", $state.snapshot(state.entry));
  443. return;
  444. }
  445. await invoke("update_request_body", {
  446. id: state.entry!!.body.id,
  447. body: { Null: null },
  448. });
  449. state.entry.body = null;
  450. console.debug("Deleted request body");
  451. }
  452. export async function updateBodyContent(body: string, ct: string) {
  453. if (state.entry!!.body != null) {
  454. await invoke("update_request_body", {
  455. id: state.entry!!.body.id,
  456. body: {
  457. Value: {
  458. ty: ct,
  459. content: body,
  460. },
  461. },
  462. });
  463. state.entry.body.body = body;
  464. state.entry.body.content_type = ct;
  465. } else {
  466. const b = await invoke("insert_request_body", {
  467. entryId: state.entry!!.id,
  468. body: {
  469. ty: ct,
  470. content: body,
  471. },
  472. });
  473. state.entry!!.body = b;
  474. }
  475. console.debug("Updated body content to", $state.snapshot(state.entry!!.body));
  476. }
  477. export async function createAuth(type: AuthType) {
  478. const auth = await invoke<Authentication>("insert_auth", {
  479. workspaceId: state.workspace!!.id,
  480. type,
  481. });
  482. console.debug("created auth", auth);
  483. state.auth.unshift(auth);
  484. }
  485. export async function renameAuth(id: number, name: string) {
  486. await invoke<Authentication>("rename_auth", {
  487. id,
  488. name,
  489. });
  490. const auth = state.auth.find((a) => a.id === id);
  491. auth!!.name = name;
  492. }
  493. export async function updateAuthParams(id: number) {
  494. const auth = state.auth.find((a) => a.id === id);
  495. console.debug("updating auth params", $state.snapshot(auth));
  496. if (!auth) {
  497. console.warn("Attempted to update non-existing auth", id);
  498. return;
  499. }
  500. console.log($state.snapshot(auth.params));
  501. await invoke<Authentication>("update_auth", {
  502. id,
  503. params: auth.params,
  504. });
  505. }
  506. export async function deleteAuth(id: number) {
  507. console.debug("deleting auth", id);
  508. await invoke("delete_auth", { id });
  509. state.auth = state.auth.filter((a) => a.id !== id);
  510. }
  511. export async function setEntryAuth(id: number | null, inherit: boolean | null) {
  512. console.debug("setting entry auth to", id, "inheriting:", inherit);
  513. await invoke("set_workspace_entry_auth", {
  514. entryId: state.entry!!.id,
  515. authId: id,
  516. inherit,
  517. });
  518. state.entry!!.auth = id;
  519. state.indexes[state.entry!!.id].auth = id;
  520. if (inherit != null) {
  521. state.entry!!.auth_inherit = inherit;
  522. state.indexes[state.entry!!.id].auth_inherit = inherit;
  523. }
  524. }
  525. type WorkspaceEntryResponse =
  526. | {
  527. type: "Collection";
  528. data: WorkspaceEntryBase;
  529. }
  530. | {
  531. type: "Request";
  532. data: WorkspaceRequestResponse;
  533. };
  534. type WorkspaceRequestResponse = {
  535. entry: WorkspaceEntryBase;
  536. method: string;
  537. url: string;
  538. body: RequestBody | null;
  539. headers: RequestHeader[];
  540. path_params: RequestPathParam[];
  541. };