state.svelte.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. import { 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. } from "./types";
  13. import { getSetting, setSetting } from "./settings.svelte";
  14. export type WorkspaceState = {
  15. /**
  16. * Currently selected workspace.
  17. */
  18. workspace: Workspace | null;
  19. /**
  20. * Currently selected workspace entry.
  21. */
  22. entry: WorkspaceEntry | null;
  23. /**
  24. * Workspace root entries.
  25. */
  26. roots: number[];
  27. /**
  28. * Workspace entry parent => children mappings.
  29. */
  30. children: Record<number, number[]>;
  31. /**
  32. * All workspace entries.
  33. */
  34. indexes: Record<number, WorkspaceEntry>;
  35. /**
  36. * Currently selected workspace environments.
  37. */
  38. environments: WorkspaceEnvironment[];
  39. /**
  40. * Currently selected environment.
  41. */
  42. environment: WorkspaceEnvironment | null;
  43. };
  44. export const state: WorkspaceState = $state({
  45. workspace: null,
  46. entry: null,
  47. roots: [],
  48. children: {},
  49. indexes: {},
  50. environments: [],
  51. environment: null,
  52. });
  53. const index = (entry: WorkspaceEntry) => {
  54. console.log("indexing", entry);
  55. state.indexes[entry.id] = entry;
  56. if (entry.parent_id !== null) {
  57. if (state.children[entry.parent_id]) {
  58. state.children[entry.parent_id].push(entry.id);
  59. } else {
  60. state.children[entry.parent_id] = [entry.id];
  61. }
  62. } else {
  63. state.roots.push(entry.id);
  64. }
  65. };
  66. function reset() {
  67. state.children = {};
  68. state.indexes = {};
  69. state.roots = [];
  70. state.entry = null;
  71. state.environment = null;
  72. state.environments = [];
  73. }
  74. export async function selectEnvironment(id: number | null) {
  75. if (id === null) {
  76. state.environment = null;
  77. return;
  78. }
  79. state.environment = state.environments.find((e) => e.id === id) ?? null;
  80. let env = await getSetting("lastEnvironment");
  81. if (env) {
  82. env[state.workspace!!.id] = id;
  83. } else {
  84. env = { [state.workspace!!.id]: id };
  85. }
  86. setSetting("lastEnvironment", env);
  87. console.debug("selected environment:", state.environment?.name);
  88. }
  89. export function selectWorkspace(ws: Workspace) {
  90. console.debug("selecting workspace:", ws.name);
  91. state.workspace = ws;
  92. }
  93. export async function selectEntry(id: number) {
  94. state.entry = state.indexes[id];
  95. console.log("selected entry:", $state.snapshot(state.entry));
  96. if (state.entry.parent_id !== null) {
  97. let parent = state.indexes[state.entry.parent_id];
  98. while (parent) {
  99. parent.open = true;
  100. if (parent.parent_id === null) {
  101. break;
  102. }
  103. parent = state.indexes[parent.parent_id];
  104. }
  105. }
  106. if (state.entry.type === "Request") {
  107. parseUrl(state.entry!!.url)
  108. .then(() =>
  109. console.debug("working URL:", $state.snapshot(state.entry.workingUrl)),
  110. )
  111. .catch((e) => {
  112. console.error("error parsing URL", e);
  113. });
  114. expandUrl()
  115. .then(() =>
  116. console.debug(
  117. "expanded URL:",
  118. $state.snapshot(state.entry.expandedUrl),
  119. ),
  120. )
  121. .catch((e) => {
  122. console.error("error expanding URL", e);
  123. });
  124. }
  125. }
  126. // COMMANDS
  127. export async function createWorkspace(name: string): Promise<Workspace> {
  128. return invoke<Workspace>("create_workspace", { name });
  129. }
  130. export async function listWorkspaces(): Promise<Workspace[]> {
  131. return invoke<Workspace[]>("list_workspaces");
  132. }
  133. export async function loadWorkspace(ws: Workspace) {
  134. reset();
  135. state.workspace = ws;
  136. const entries = await invoke<WorkspaceEntryResponse[]>(
  137. "list_workspace_entries",
  138. {
  139. id: state.workspace.id,
  140. },
  141. );
  142. for (const entry of entries) {
  143. if (entry.type === "Request") {
  144. index({
  145. ...entry.data.entry,
  146. method: entry.data.method,
  147. url: entry.data.url,
  148. headers: entry.data.headers,
  149. body: entry.data.body,
  150. path: entry.data.path_params,
  151. });
  152. } else {
  153. index(entry.data);
  154. }
  155. }
  156. await loadEnvironments(state.workspace.id);
  157. }
  158. export function createRequest(parentId?: number) {
  159. if (state.workspace == null) {
  160. console.warn("create workspace request called with no active workspace");
  161. return;
  162. }
  163. const data = {
  164. Request: {
  165. name: "",
  166. workspace_id: state.workspace.id,
  167. parent_id: parentId,
  168. method: "GET",
  169. url: "",
  170. },
  171. };
  172. invoke<WorkspaceEntryBase>("create_workspace_entry", {
  173. data,
  174. }).then((entry) => {
  175. index({
  176. ...entry,
  177. method: data.Request.method,
  178. url: data.Request.url,
  179. body: null,
  180. headers: [],
  181. path: [],
  182. });
  183. console.log("request created:", entry);
  184. });
  185. }
  186. export function createCollection(parentId?: number) {
  187. if (state.workspace == null) {
  188. console.warn("create workspace request called with no active workspace");
  189. return;
  190. }
  191. const data = {
  192. Collection: {
  193. name: "",
  194. workspace_id: state.workspace.id,
  195. parent_id: parentId,
  196. },
  197. };
  198. invoke<WorkspaceEntryBase>("create_workspace_entry", {
  199. data,
  200. }).then((entry) => {
  201. index(entry);
  202. console.log("collection created:", entry);
  203. });
  204. }
  205. export async function loadEnvironments(workspaceId: number) {
  206. state.environments = await invoke<WorkspaceEnvironment[]>(
  207. "list_environments",
  208. { workspaceId },
  209. );
  210. const lastEnv = (await getSetting("lastEnvironment"))?.[workspaceId];
  211. if (lastEnv) {
  212. selectEnvironment(lastEnv);
  213. }
  214. }
  215. export async function createEnvironment(workspaceId: number, name: string) {
  216. console.debug("creating environment in", workspaceId);
  217. const env = await invoke<WorkspaceEnvironment>("create_env", {
  218. workspaceId,
  219. name,
  220. });
  221. state.environment = env;
  222. state.environments.push(state.environment);
  223. }
  224. export async function updateEnvironment() {
  225. if (!state.environment) {
  226. console.warn("attempted to persist null env");
  227. return;
  228. }
  229. console.debug("updating environment", state.environment);
  230. await invoke("update_env", {
  231. id: state.environment.id,
  232. name: state.environment.name,
  233. });
  234. }
  235. export async function sendRequest(): Promise<any> {
  236. const res = await invoke("send_request", {
  237. reqId: state.entry!!.id,
  238. envId: state.environment?.id,
  239. });
  240. console.debug(res);
  241. return res;
  242. }
  243. export async function updateEntryName(name: string) {
  244. if (!state.entry) {
  245. console.warn("attempted to persist null entry");
  246. return;
  247. }
  248. console.debug(state.entry.id, "updating entry name to", name);
  249. const data =
  250. state.entry.type === "Request"
  251. ? {
  252. Request: {
  253. base: {
  254. name,
  255. },
  256. },
  257. }
  258. : { Collection: { name } };
  259. await invoke("update_workspace_entry", {
  260. entryId: state.entry.id,
  261. data,
  262. });
  263. }
  264. export async function parseUrl(url: string) {
  265. console.debug("parsing", $state.snapshot(url));
  266. state.entry!!.workingUrl = await invoke<RequestUrl>("parse_url", {
  267. url,
  268. envId: state.environment?.id,
  269. });
  270. }
  271. /**
  272. * Update a request's URL string. If `usePathparams` is true, path entries
  273. * from state.entry.path will be used to replace those at the same position and should
  274. * be set to true whenever this is called from an input field of a destructured URL.
  275. */
  276. export async function updateUrl(u: string, usePathParams: boolean) {
  277. const [url, params] = await invoke<any[]>("update_url", {
  278. entryId: state.entry!!.id,
  279. usePathParams,
  280. url: u,
  281. pathParams: state.entry.path,
  282. });
  283. state.entry!!.url = u;
  284. state.entry.path = params;
  285. state.entry.workingUrl = url;
  286. expandUrl();
  287. console.debug("updated", $state.snapshot(state.entry));
  288. }
  289. export async function expandUrl() {
  290. state.entry!!.expandedUrl = await invoke<string>("expand_url", {
  291. entryId: state.entry!!.id,
  292. envId: state.environment?.id,
  293. url: state.entry!!.url,
  294. });
  295. }
  296. export async function insertEnvVariable(
  297. workspaceId: number,
  298. envId: number,
  299. name: string = "",
  300. value: string = "",
  301. secret: boolean = false,
  302. ) {
  303. const v = await invoke<EnvVariable>("insert_env_var", {
  304. workspaceId,
  305. envId,
  306. name,
  307. value,
  308. secret,
  309. });
  310. state.environment?.variables.push(v);
  311. }
  312. export async function updateEnvVariable(v: EnvVariable) {
  313. if (v.name.length === 0 && v.value.length === 0) {
  314. console.debug("deleting var:", v);
  315. return deleteEnvVariable(v.id);
  316. }
  317. console.debug("updating var:", v);
  318. return invoke("update_env_var", {
  319. id: v.id,
  320. name: v.name,
  321. value: v.value,
  322. secret: v.secret,
  323. });
  324. }
  325. export async function deleteEnvVariable(id: number) {
  326. await invoke("delete_env_var", { id });
  327. state.environment!!.variables = state.environment!!.variables.filter(
  328. (v) => v.id !== id,
  329. );
  330. }
  331. export async function insertHeader() {
  332. const header = await invoke("insert_header", {
  333. entryId: state.entry!!.id,
  334. insert: { name: "", value: "" },
  335. });
  336. state.entry!!.headers.push(header);
  337. }
  338. export async function updateHeader(id: number, name: string, value: string) {
  339. await invoke("update_header", {
  340. update: { id, name, value },
  341. });
  342. const header = state.entry!!.headers.find((header) => header.id === id);
  343. header.name = name;
  344. header.value = value;
  345. }
  346. export async function deleteHeader(id: number) {
  347. await invoke("delete_header", {
  348. headerId: id,
  349. });
  350. state.entry!!.headers = state.entry!!.headers.filter(
  351. (header) => header.id !== id,
  352. );
  353. }
  354. export async function deleteBody() {
  355. if (state.entry!!.body === null) {
  356. console.warn("attempted to delete null body", $state.snapshot(state.entry));
  357. return;
  358. }
  359. await invoke("update_request_body", {
  360. id: state.entry!!.body.id,
  361. body: { Null: null },
  362. });
  363. state.entry.body = null;
  364. console.debug("Deleted request body");
  365. }
  366. export async function updateBodyContent(body: string, ct: string) {
  367. if (state.entry!!.body !== null) {
  368. await invoke("update_request_body", {
  369. id: state.entry!!.body.id,
  370. body: {
  371. Value: {
  372. ty: ct,
  373. content: body,
  374. },
  375. },
  376. });
  377. state.entry.body.body = body;
  378. state.entry.body.content_type = ct;
  379. } else {
  380. const b = await invoke("insert_request_body", {
  381. entryId: state.entry!!.id,
  382. body: {
  383. ty: ct,
  384. content: body,
  385. },
  386. });
  387. state.entry!!.body = b;
  388. }
  389. console.debug("Updated body content to", $state.snapshot(state.entry!!.body));
  390. }
  391. type WorkspaceEntryResponse =
  392. | {
  393. type: "Collection";
  394. data: WorkspaceEntryBase;
  395. }
  396. | {
  397. type: "Request";
  398. data: WorkspaceRequestResponse;
  399. };
  400. type WorkspaceRequestResponse = {
  401. entry: WorkspaceEntryBase;
  402. method: string;
  403. url: string;
  404. body: RequestBody | null;
  405. headers: RequestHeader[];
  406. path_params: RequestPathParam[];
  407. };