state.svelte.ts 16 KB

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